---
title: Schemas and validation
description: Start with any JSON, then add a JSON Schema so bad data is rejected. Versions are kept, and activation checks your existing records first.
group: Working with data
order: 4
keywords: [json schema, validation, data model, schema versioning, migration]
---

# Schemas and validation

A new collection takes any JSON. That is deliberate: when you are still figuring out
what you are building, a schema is in the way.

Once the shape settles, add a schema and the API starts rejecting anything that does not
match — a typo in a field name, a number sent as text, a missing title.

## Add one from the dashboard

Open the collection, go to the **Schema** tab, and add fields with the builder: name,
type, required or not. Or switch to the raw editor and paste
[JSON Schema](https://json-schema.org/) directly.

```json
{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "type": "object",
  "additionalProperties": false,
  "properties": {
    "title": { "type": "string", "minLength": 1 },
    "done": { "type": "boolean" },
    "priority": { "type": "string", "enum": ["low", "medium", "high"] },
    "tags": { "type": "array", "items": { "type": "string" } },
    "due": { "type": ["string", "null"], "format": "date" }
  },
  "required": ["title", "done"]
}
```

`additionalProperties: false` is the strict choice: anything you did not list is
rejected. Leave it out while you are still adding fields.

## Add one from the API

```bash
curl -s -X POST "$BAAS_URL/workspaces/$BAAS_WS/collections/todos/schemas" \
  -H "Authorization: Bearer $BAAS_KEY" -H "Content-Type: application/json" \
  -d '{"schema": { … }, "migrationNotes": "require a title"}'
```

Needs `schemas:write`.

## What happens to records that break the new rules

This is the part people worry about, and the answer is: nothing, unless you say so.

Before a version is switched on, every existing record is checked. If any would become
invalid, activation is **refused** with `422 SCHEMA_MIGRATION_BLOCKED` and a report of
what failed, so you can fix the data first.

Check before you commit to it:

```bash
curl -s -X POST "$BAAS_URL/workspaces/$BAAS_WS/collections/todos/schemas/validate" \
  -H "Authorization: Bearer $BAAS_KEY" -H "Content-Type: application/json" \
  -d '{"schema": { … }}'
```

```json
{ "data": { "recordsChecked": 120, "validRecords": 118, "invalidRecords": 2,
            "samples": [ { "recordId": "01a1…", "issues": [ … ] } ] } }
```

If you genuinely want to go ahead anyway, pass `"allowInvalidRecords": true`.

Activating a schema does **not** rewrite your old records. Each keeps the
`schemaVersion` it was last checked against, and is validated against the current schema
the next time it is written.

## Versions

Schema history is append-only: versions are added, never edited or deleted. Each records
who made it, when, and your migration note.

```bash
# stage a version without switching it on
-d '{"schema": { … }, "activate": false}'

# switch one on later, or roll back to an older one
curl -s -X POST "…/schemas/2/activate" -H "Authorization: Bearer $BAAS_KEY"
curl -s -X POST "…/schemas/1/activate" -H "Authorization: Bearer $BAAS_KEY"

# go back to accepting anything (history is kept)
curl -s -X POST "…/schemas/deactivate" -H "Authorization: Bearer $BAAS_KEY"
```

## When a write is rejected

```json
{
  "error": {
    "code": "SCHEMA_VALIDATION_FAILED",
    "message": "Record does not match collection schema",
    "details": {
      "issues": [
        { "path": "/email", "message": "must match format \"email\"", "keyword": "format" },
        { "path": "/title", "message": "is required", "keyword": "required" }
      ],
      "schemaVersion": 3
    }
  }
}
```

`path` is a pointer into your `data`, so `/email` means the `email` field. These map
cleanly onto form fields if you are showing errors to a user.

## What JSON Schema features work

Supported: the usual types, nested objects, arrays (including arrays of objects), enums,
`const`, nullable types like `["string","null"]`, required and optional fields, number
and string constraints, `pattern`, `format`, the combinators `allOf` / `anyOf` / `oneOf`
/ `not` / `if` / `then` / `else`, and local `$defs` with `$ref`. A schema can also
describe a non-object, such as an array of numbers.

A schema is checked before it is accepted, and refused if it would not do what you
expect:

- it must be valid JSON Schema draft 2020-12;
- unknown keywords are rejected, so `requird` is an error rather than a rule that
  silently does nothing;
- only local `$ref`s — nothing is fetched from the internet;
- regular expressions are length-limited and screened for catastrophic backtracking,
  because they run on every write;
- only known `format` values, so a typo cannot disable a check;
- at most 64 KB and 32 levels deep.

Anything else comes back as `400 INVALID_SCHEMA` with the specific problem.

## Hints for the dashboard's form editor

`x-ui` keywords are ignored by validation but make the generated form nicer:

```json
{ "description": { "type": "string", "x-ui": { "label": "Notes", "multiline": true } } }
```

## Next steps

- [Records](/docs/records) — writing data that matches.
- [Errors](/docs/errors) — the full error list.
