Embedded schemas
Embedded documentSchema
DocumentInput accepts either a schemaRef (pointer to a built-in
schema) or an embedded JSON Schema body via documentSchema. This page
documents how documentSchema is handled server-side, what JSON Schema
features are supported, and the composition patterns that work.
schemaRef vs documentSchema — which do I use?
| Situation | Use |
|---|---|
| Data matches a single built-in exactly | schemaRef |
| Need to combine several built-ins (e.g. name+address) | documentSchema (with $ref) |
| Need extra required fields on top of a built-in | documentSchema (allOf + required) |
| Need fields that no built-in covers | documentSchema |
| One-off document tied to a specific flow or app | documentSchema |
Warning
Mutual exclusion is enforced. Providing neither returns a “no schema
definition” error; providing both returns “both schema definitions”. The
check lives in DocumentMetadata.ValidateSchemaDefinition() in the domain
layer (pkg/domain/document.go).
Lifecycle contract
- At create time — the backend parses
documentSchemaas a JSON Schema, registers it at URIembedded://document-schema.json, compiles it, and validatesdataagainst it. Failure → GraphQL error, nothing is persisted. - At write time — the compiled schema is stored as
byteaalongside the encrypted data. The schema itself is not encrypted (it’s metadata, not data). - On
update— the schema is preserved verbatim from the existing version. The backend ignoresdocumentSchema/schemaRefon update inputs. If you need a different schema, create a new document. - On
validate— same pipeline as create, but the document is never persisted. Useful for client-side pre-flight checks.
Supported JSON Schema
The compiler is santhosh-tekuri/jsonschema v6
running in strict mode (AssertContent, AssertFormat, AssertVocabs).
Supported: JSON Schema Draft 2020-12, including:
- Standard keywords:
type,properties,required,additionalProperties,patternProperties,allOf,anyOf,oneOf,not,if/then/else,enum,const,pattern,minLength/maxLength,minimum/maximum/exclusiveMinimum/exclusiveMaximum,multipleOf,minItems/maxItems,uniqueItems,minProperties/maxProperties $ref,$defs,$anchor,$dynamicRef- Nested arrays (
items,prefixItems,contains) - Discrimination via
oneOf+$defs
Custom string format values
The compiler registers two non-standard formats on top of the library’s built-in ones:
format |
Validation |
|---|---|
country |
ISO 3166-1 alpha-2 or alpha-3 (via the countries Go package) |
currency |
Exactly 3 uppercase ASCII letters (ISO 4217) |
email |
Library default (RFC 5322) |
date |
Library default (ISO 8601 date) |
date-time |
Library default |
uri |
Library default |
Formats are asserted — a value that violates the format fails validation with a GraphQL error. They are not just annotations.
$ref to built-in schemas
This is the reason you’d usually reach for documentSchema. The compiler
has the schema registry wired in as a URL loader, so any $ref to a
canonical built-in URL is resolved from the embedded filesystem:
No network is used. External URLs (e.g. https://example.com/foo.json)
are rejected at compile time with “schema not found”.
See Built-in schemas for the full list of 24 resolvable refs.
Examples
1. Minimal inline schema — no refs
A free-form profile with no built-in fields:
With data:
2. Extend a built-in with required fields
The built-in PersonAddress marks every field as optional. If your app
wants streetAddress, city, postalCode, and addressCountry to be
mandatory, wrap it in allOf + required:
The built-in’s format: "country" on addressCountry still applies, so
"addressCountry": "INVALID" fails.
3. Compose multiple built-ins
Combine PersonFullName and PersonAddress into a single document with
both nested under named properties:
With data:
4. Extend with your own properties
Add app-specific fields alongside a built-in using the “second allOf
branch” pattern:
5. Discrimination with $defs + oneOf
When a document can be one of several shapes, model it like the built-in
UBOStructure does — define variants in $defs and discriminate with a
fixed ownerType enum:
6. Enum-constrained field
For fixed vocabularies, pair type: "string" with enum:
Passing documentSchema over GraphQL
documentSchema is typed as JSON — a json.RawMessage on the wire. You
can send it as a JSON object literal in variables:
Use user.document.validate
with the same input shape to dry-run the schema + data before creating.
Gotchas
- Schema is locked at create.
updatekeeps the original schema. There is no “migrate schema” API; create a new document instead. uniquePerVaultis locked too and enforced by a PostgreSQL unique constraint. If you setuniquePerVault: trueand a second create would collide, the write fails withdocument schema already exists in vault. The uniqueness is per(vault, schemaRef)or(vault, documentSchema).- Only registered URLs are resolvable.
$refto a built-in URL works.$refto any other absolute URL errors with “schema not found”. Relative$refinto#/$defswithin the same schema works. - Strict format assertion.
format: "country"on a value that isn’t a valid ISO 3166 code fails. Library defaults (email,uri,date) are similarly strict. - No additionalProperties by default. JSON Schema’s default is to
allow any extra properties. If you want strict rejection, set
"additionalProperties": falseexplicitly on the outermost object. - Schema is not encrypted. It is metadata — only the
datapayload is encrypted with the vault’s DEK. Don’t put secrets indescriptionor in the schema itself. - Size. Schemas are stored as
byteainvault_documents.document_schema. There is no hard upper bound, but validation time scales with schema complexity. Keep schemas focused.