4.5 Schemas, validation errors, and JSON merge PATCH

When you want your records to have a guaranteed shape, install a JSON Schema on the dataset and turn on enforceSchema. The data service then validates every write (and every PATCH result) and rejects bad data with a precise, machine-readable error.

Installing a schema

A schema is a fixed child of the dataset called .schema.json, written with the store contract:

curl.exe -X PUT http://127.0.0.1:3100/data/people/.schema.json `
  -H "Content-Type: application/schema+json" `
  --data '{ "type": "object", "required": ["name"],
            "properties": { "name": {"type":"string"},
                            "age":  {"type":"integer","minimum":0} } }'

The PUT compile-checks the schema first, so a malformed schema is rejected at install time rather than failing later on a write. Validators are compiled and cached, so enforcement is cheap.

Validation on write

With enforceSchema: true, a write that violates the schema fails with 422 validation_failed and an errors array pinpointing each problem:

{ "code": "validation_failed", "status": 422,
  "errors": [ { "path": "/age", "error": "must be >= 0" } ] }

The path is a JSON Pointer into the record, so a client (or a form UI) can map the error straight to a field.

Schema-carrying content type

When a record has a schema, reads advertise it:

Content-Type: application/json; schema="…/people/.schema.json"
Link: <…/people/.schema.json>; rel="describedby"

So a client can discover and fetch the rules a record obeys without out-of-band knowledge — the same schema the runtime enforces is the one it points you to (no drift).

JSON merge PATCH

PATCH applies an RFC 7386 JSON merge patch: the body is merged into the existing record (a null value deletes a field), the merged result is re-validated, and the new record is returned.

curl.exe -X PATCH http://127.0.0.1:3100/data/people/ada `
  -H "Content-Type: application/json" --data '{ "age": 36 }'

Because the result is validated, a PATCH can't sneak a record into an invalid state. If the merge would violate the schema, you get the same 422 validation_failed with the offending paths, and the stored record is untouched.

When to enforce

Turn enforceSchema on for datasets that back forms, APIs, or anything where a malformed record would cause downstream bugs. Leave it off for free-form or scratch data. It's a per-mount setting, so you can run strict and loose data mounts side by side.

Field-level access (fieldLevelAuthz)

Mount access (5.4) gates a whole record. To restrict individual fields, set fieldLevelAuthz: true on the data mount and annotate properties in the schema with x-rs-read / x-rs-write — role specs in the same grammar as mount access (all, authenticated, a role name, or role /path/{email}):

{ "type": "object", "properties": {
    "name":  { "type": "string" },
    "roles": { "type": "string", "x-rs-write": "A" },          // only A may change it
    "ssn":   { "type": "string", "x-rs-read": "A", "x-rs-write": "A" }
} }
  • Reads redact. A GET omits each field whose x-rs-read the caller doesn't satisfy — they get the rest of the record. The ETag is computed over the stored (unredacted) record, so concurrency is stable.
  • Writes reject. Changing a field the caller can read but can't write is 403. Fields the caller can't read are never touched — they're preserved from the stored record — so a read→edit→write-back can't drop data it couldn't see. PATCH is the natural edit path: it merges against the full stored record, so a redacted field is simply absent and preserved.
  • The schema is policy, so editing it needs an operator. Because the rules live in .schema.json, on a fieldLevelAuthz mount writing the schema requires a tenant operator (5.0) — otherwise the rules could be removed to bypass them.

This is how you let users edit their own record without letting them grant themselves privileged roles: annotate the users dataset's roles field x-rs-write: "<operator role>". Unannotated fields are unrestricted (mount access is still the coarse gate). Top-level fields only for now; data exposed through a query mount bypasses field redaction (gate sensitive datasets at the mount there).

Last in this part: the listing/pagination details and the delete guard shared by all stores.


Previous: 4.4 The data service · Manual home · Next: 4.6 Listings, pagination, delete guard →