---
title: Schema Validation
description: Declarative per-collection rules — required fields, field types, foreign-id references, and unique constraints — enforced on writes and stored in the op-log.
---

OMGDB supports lightweight, declarative schema validation per collection. A collection can carry a *spec* that lists which fields are **required**, the expected **type** of named fields, **foreign-`_id` references** to other collections, and **unique** fields (single or compound). Rules are enforced when documents are written, and they live in the op-log as a `define` operation — so a packed or replayed store carries its own validation rules and remains self-describing.

Validation is deliberately narrow: the vocabulary is `required` / `fields` / `refs` / `unique`, nothing more. There is no JSON-Schema-style constraint language (no patterns, ranges, enums, or nested sub-schemas). See [data model](/docs/data-model/) for the value types these rules reference.

## Defining a collection spec

A spec is a JSON (or YAML) object describing one or more collections:

```json
{
  "collections": {
    "users": {
      "required": ["name"],
      "fields": { "age": "long", "bio": "markdown" },
      "unique": ["email", ["tenant", "email"]]
    },
    "posts": {
      "fields": { "author": { "type": "objectId", "ref": "users" } },
      "refs": { "reviewer": "users" }
    }
  }
}
```

- `required` — an array of field names that must be present on every document. Each entry must be a string.
- `fields` — an object mapping a field name to its expected type: either a type-name string (see [the type table](#supported-field-type-names)) or an object `{ "type": ..., "ref": ... }` combining a type with a reference.
- `refs` — an object mapping a field name to the collection whose `_id` it must reference (a collection-name string, or `{ "collection": "users", "field": "_id" }` — only `_id` is supported as the target field).
- `unique` — an array of uniqueness constraints: a field-name string for a single-field constraint, or an array of field names for a compound one.

All keys are optional. A collection with none of them imposes no constraints.

You attach a spec to a store at creation time with `--from`:

```sh
omgdb create app.omgdb --from spec.json
```

The CLI reads the spec file, defines each entry in the top-level `collections` object on the new store, and creates a unique index for every `unique` entry. On success it prints what it did:

```text
created store at app.omgdb (2 collection(s) defined, 2 unique index(es))
```

> **Note:** The spec file may be JSON or YAML — a `.yaml`/`.yml` extension selects a minimal YAML subset (mappings, sequences, inline arrays, `#` comments; tabs and multi-line block scalars are not supported). The top level must contain a `collections` object; otherwise `omgdb create --from` fails with ``spec must contain a `collections` object``.

Defining a collection is **idempotent** with respect to replay: each `define` is recorded as a `define` op in the log, and replaying the log rebuilds the validation catalog. Re-defining a collection replaces its rules. Because the rules are persisted in the log, reopening, packing, or compacting the store preserves them — the database describes its own constraints. `omgdb describe` renders each collection's spec (and its indexes) as part of the live manual — see [introspection](/docs/introspection/).

## Supported field type names

A field's expected type is given as one of the type-name strings below. The vocabulary is **checked when the spec is defined**: an unknown or misspelled type fails at define time with ``field `age` has unknown type `lng` (expected one of: array, binData, bool, date, double, long, markdown, null, number, object, objectId, string)`` — instead of silently producing a spec no insert could ever satisfy.

| Type name  | Value kind | Example value |
| ---------- | ---------- | ------------- |
| `null`     | JSON null  | `null` |
| `bool`     | Boolean    | `true` |
| `long`     | 64-bit signed integer | `42` |
| `double`   | 64-bit float | `3.14` |
| `number`   | Any numeric value — matches both `long` and `double` | `42` or `3.14` |
| `string`   | UTF-8 string | `"ada"` |
| `markdown` | Markdown text — validated as a string | `"# Title\n\nBody"` |
| `binData`  | Binary data | `{"$binary":"..."}` |
| `array`    | Array      | `[1, 2, 3]` |
| `object`   | Embedded document | `{"k": 1}` |
| `objectId` | 12-byte ObjectId | `{"$oid":"..."}` |
| `date`     | Timestamp (ms since epoch) | `{"$date": 0}` |

> **Note:** A bare JSON integer such as `36` is a `long`, while `36.0` is a `double` — they are distinct types, and a field declared `long` rejects a value written with a decimal point. Declare `number` when either numeric kind is acceptable. `markdown` declares intent — the field holds Markdown text, as produced by [Markdown import](/docs/markdown/) — and the type check enforces that the value is a string.

## How rules are enforced

Validation runs **on write**, at the moment a document enters a collection that has a spec:

- `Store::insert_one` validates the document before it is appended to the log. A violation returns a validation error and writes nothing.
- Inserts inside a [transaction](/docs/transactions/) (`Txn::insert_one`) are validated the same way; a violation makes the closure return an error, which aborts the whole transaction.
- `Store::replace_one` (and `Txn::replace_one`) validate the replacement document, so an update cannot move a document into an invalid state.

The check covers:

1. **Required fields** — every name in `required` must be present on the document. A missing one fails with ``missing required field `name` ``.
2. **Field types** — for each field present on the document that also appears in `fields`, the value's actual type must match the declared type. A mismatch fails with ``field `age` should be `long` but is `string` ``.
3. **References** — for each field with a `ref`, the value must be the `_id` of an existing document in the referenced collection. A `null` value passes (an optional reference), an array is checked element by element, and a dangling reference fails with ``field `author` references missing `users._id` ...``.
4. **Uniqueness** — each `unique` entry is enforced by a unique secondary index (single-field or compound), so a write that duplicates an existing key is rejected. See [indexes](/docs/indexes/) for unique-index semantics.

> **Limitation:** Type rules apply only to fields that are *present*. A field listed in `fields` but not in `required` is unconstrained when absent — declaring a type does not make a field mandatory. Use `required` for presence and `fields` for typing. There is no constraint on fields that appear in a document but not in the spec; extra fields are always allowed.

### A passing and a failing insert

Given the `users` spec above (`required: ["name"]`, `fields: {"age": "long"}`):

```sh
# Passes: name is present, age is a long.
omgdb insert app.omgdb users '{"name":"ada","age":36}'

# Fails: missing the required field `name`.
omgdb insert app.omgdb users '{"age":5}'

# Fails: `age` is declared `long` but the value is a string.
omgdb insert app.omgdb users '{"name":"x","age":"old"}'
```

A passing insert prints the assigned `_id` (an ObjectId, e.g. `{"$oid":"..."}`). A failing insert is rejected with a validation error and leaves the store unchanged.

## Auditing existing documents

Specs are enforced going forward, but a collection may already hold documents — for example a spec defined after data was inserted, or documents introduced by an external producer that appended to the log. The `validate` command scans a collection and reports every document that does **not** satisfy the current rules (required fields, types, and references):

```sh
omgdb validate app.omgdb users
```

When every document is valid (or the collection has no rules) it prints a single success line:

```text
valid: no violations in `users`
```

When there are offenders it prints one line per violating document — the document's `_id` as canonical JSON, followed by the reason — and exits non-zero:

```text
{"$oid":"6630c0..."}: missing required field `name`
{"$oid":"6630c1..."}: field `age` should be `long` but is `string`
2 document(s) violate validation rules
```

Each document is reported with the first rule it violates. `validate` is read-only: it never modifies documents or the log, so it is safe to run as an audit step in CI or before compaction.

## End-to-end example

```sh
# spec.json: {"collections":{"users":{"required":["name"],"fields":{"age":"long"},"unique":["email"]}}}
omgdb create app.omgdb --from spec.json
omgdb insert app.omgdb users '{"name":"ana","age":30,"email":"ana@example.com"}'
omgdb insert app.omgdb users '{"name":"bob","email":"ana@example.com"}'  # rejected: duplicate email
omgdb validate app.omgdb users   # -> valid: no violations in `users`
```

The rules survive reopening the store: because the `define` op is in the log, a fresh `omgdb` process replays it and enforces the same constraints. To see the inferred shape of a collection alongside its declared rules and indexes, use [introspection](/docs/introspection/) via `omgdb describe`.

## Limitations

> **Limitation:** Validation is intentionally narrow and covers only what is listed here.

- The vocabulary is `required` (presence), `fields` (types), `refs` (foreign `_id` references), and `unique` (single/compound uniqueness). No ranges, regex/patterns, enums, length limits, or conditional rules — this is not JSON Schema.
- Type checks apply to the top-level field's value type. There is no coercion (a `double` is not accepted where a `long` is declared — declare `number` for either) and no validation of array element types or nested sub-document fields.
- References target only the `_id` of the referenced collection, and referential integrity is checked on write — deleting a referenced document is not blocked retroactively.
- Rules apply to `insert` and `replace`. They are not re-checked retroactively when a spec changes — use `omgdb validate` to find pre-existing offenders.

For more on the value types referenced by `fields`, see [data model](/docs/data-model/). For a hands-on walkthrough of creating a store and inserting documents, see the [quickstart](/docs/quickstart/).
