Skip to content
Talk to our solutions team

Schema export and discovery

data.svc renders eight external schema formats and a JSON discovery surface from the same entity definitions the engine executes. Nothing is hand-maintained: change a field, rebuild the engine, and every format changes with it.

Export renderers are pure functions over the resolved schema — no I/O, no state, and deterministic ordering (entities and enums sorted by name, fields by declaration position with a name tie-break). The JSON-valued formats are byte-identical across calls, so they diff and cache cleanly. The Zod and CUE files stamp the generation time into a header comment, so two fetches of an unchanged schema differ on that line — strip the header before diffing.

Paths are shown as an external client sees them through the gateway, which strips the /data/ prefix; the service itself serves them at /schema/.... Every route accepts an optional ?datastore=<name> and falls back to the schema’s default datastore.

FormatVersionBundle endpointPer-entity endpointContent type
OpenAPI3.1.0GET /data/schema/openapi.jsonapplication/json
JSON Schema2020-12GET /data/schema/jsonschema.jsonGET /data/schema/jsonschema/{entity}application/schema+json
Zod (TypeScript)Zod 3.22+GET /data/schema/zod.tsGET /data/schema/zod/{entity}application/typescript
CUEGET /data/schema/cueGET /data/schema/cue/{entity}text/x-cue
Protocol Buffersproto3nonenone
Apache Avro1.11nonenone
OData CSDLV4nonenone
AsyncAPI2.6.0nonenone

On the per-entity routes the file extension is optional — /data/schema/jsonschema/order and /data/schema/jsonschema/order.json return the same document, and the same holds for .ts on the Zod route and .cue on the CUE route.

All export and discovery routes require a bearer token. There is no anonymous mirror for schema discovery, unlike GraphQL, which serves an anonymous endpoint. A published anonymous integration has to be handed its schema out of band.

FormatUse it for
OpenAPI 3.1Generating REST clients and SDKs; import into API consoles and contract-test tooling.
JSON Schema 2020-12Request/response validation with Ajv or any 2020-12 validator; the input to most codegen chains.
ZodFront-end TypeScript types and runtime validation compiled from the same definitions.
CUEConfig templating and policy validation pipelines that need a lattice, not a validator.
Protocol BuffersTyped messages for gRPC or binary transports built alongside the API.
AvroKafka and schema-registry pipelines; the record shapes for change-feed consumers.
OData CSDLDirect binding from Excel Power Query, Tableau and SAP clients.
AsyncAPIAn event-contract document describing entity created/updated/deleted messages.

Six of the eight renderers emit three shapes for each entity; OData CSDL and AsyncAPI emit the Output shape only:

VariantContainsMeaning
OutputEvery readable field, including server-managed onesWhat a response body looks like
InputFields a client may send on createWhat a POST body looks like
PatchInput with every field optionalWhat a PATCH body looks like

Entity names are PascalCased for type names — order_item becomes OrderItem — and the variants are named per format:

FormatOutputInputPatch
OpenAPIcomponents.schemas.OrderOrderInputOrderPatch
JSON Schema$defs/Order$defs/OrderInput$defs/OrderPatch
ZodOrderSchemaOrderInputSchemaOrderPatchSchema
CUE#Order#OrderInput#OrderPatch
Protocol Buffersmessage Ordermessage OrderInputmessage OrderPatch
Avrorecord OrderOrderInputOrderPatch
OData CSDLEntityType Ordernot emittednot emitted
AsyncAPIcomponents.schemas.Ordernot emittednot emitted

Zod additionally exports the three inferred TypeScript types Order, OrderInput and OrderPatch, and derives OrderPatchSchema as OrderInputSchema.partial().

Variant membership is driven by field modifiers and the computed flag:

Field declarationOutputInputPatch
no modifieryesyesyes
readonlyyesnono
writeonlynoyesyes
finalyesyesno
computed: (virtual or materialized)yesnono
writeonceyesyesyes

writeonce is enforced at write time by the engine but has no effect on export shape.

Requiredness is per-variant: Output treats every emitted field as present, Input honours the field’s required flag, and Patch requires nothing.

A schema export is a data-egress surface, so one gate runs in front of every renderer’s field loop. A field is dropped from all three variants in all eight formats when its effective compliance posture marks it non-exportable — either explicitly, or implicitly because it carries a data classification.

DeclarationEffect on exports
compliances: [{type: noexport}]Field removed from every format and variant
compliances: [{type: pii}]Implies noexport (and nolog); field removed
compliances: [{type: phi}]Implies noexport; field removed
compliances: [{type: pci}]Implies noexport; field removed
compliances: [{type: gdpr}]Implies noexport; field removed
any other marker (mask, redact, hidden, tokenize, encrypted, hash, nolog, noindex, retention, searchable)Field exported; marker surfaces as x-compliance in the OpenAPI-flavoured schemas
entities:
- name: customer
fields:
- name: email
type: string
- name: internal_score
type: string
compliances:
- type: noexport
- name: ssn
type: string
compliances:
- type: pii

Only email appears in the rendered documents. internal_score is dropped by the explicit marker and ssn by the classification.

Field typeJSON Schema / OpenAPIZodCUE
string, textstringz.string()string
int, bigintintegerz.number().int()int
floatnumberz.number()number
decimal, moneynumberz.number()number
boolbooleanz.boolean()bool
datetimestring, format: date-timez.string().datetime()time.Time
datestring, format: datez.string().regex(/^\d{4}-\d{2}-\d{2}$/)string & =~ "^\\d{4}-\\d{2}-\\d{2}$"
timestring, format: timez.string().regex(/^\d{2}:\d{2}(:\d{2})?$/)string & =~ "^\\d{2}:\\d{2}(:\\d{2})?$"
uuidstring, format: uuid + patternz.string().uuid()string & =~ "<uuid pattern>"
ulidstring + ^[0-9A-HJKMNP-TV-Z]{26}$z.string().ulid()string & =~ "^[0-9A-HJKMNP-TV-Z]{26}$"
json{} (unconstrained)z.unknown()_
enum, state$ref: #/$defs/<Enum><Enum>Enum#<Enum>
array(<inner>)array with itemsz.array(<inner>)[...<inner>]
fileobject {id, url, mime_type, size}z.object({ id, url, mime_type, size })struct with the same four keys
bytesstring, contentEncoding: base64z.string()bytes

Zod’s native string formats require Zod 3.22 or later. The renderer has a non-native fallback, but it is not equivalent: uuid and ulid degrade to z.string().regex(…) while datetime degrades to a bare z.string() and the .email(), .url() and .ip() validations are dropped entirely. The shipped HTTP route emits the native form; the fallback is not reachable over HTTP. date and time are always regexes — Zod has no native helper for either.

Field typeProtocol BuffersAvroOData CSDL
string, textstringstringEdm.String
intint32intEdm.Int32
bigintint64longEdm.Int64
floatdoubledoubleEdm.Double
decimal, moneystringbytes + logicalType: decimalEdm.Decimal
boolboolbooleanEdm.Boolean
datetimeint64 epoch-millislong + logicalType: timestamp-millisEdm.DateTimeOffset
dateint64 epoch-millisint + logicalType: dateEdm.Date
timestringstringEdm.String
uuidstringstring + logicalType: uuidEdm.Guid
ulidstringstringEdm.String
json, filestringstringEdm.String
enum, stategenerated enum typegenerated enum typenamespace-qualified enum type
array(<inner>)repeated <inner>array with itemsCollection(<Edm.*>)
bytesbytesbytesEdm.Binary

Proto keeps decimal and money as string to preserve precision; Avro encodes them as a decimal logical type with precision 18 and scale 2 unless the renderer is told otherwise. Proto emits google.protobuf.Timestamp for date and datetime only when the well-known-types option is enabled; epoch-millis is the default.

FormatNullable field renders as
JSON Schema / OpenAPItype: [X, "null"], or anyOf: [<orig>, {type: "null"}] when the field is a $ref
Zod.nullable() (and .optional() when not required)
CUE(<expr>) | null, with a ? suffix on the key when optional
Protocol Buffersnothing — nullable is not represented. The optional keyword (proto3 field presence) is emitted for non-required non-array fields and for every Patch field
Avrounion ["null", <type>] with "default": null
OData CSDLNullable="true"; "false" is emitted only when the field is required and not nullable

The document describes the REST surface. It emits openapi: "3.1.0", an info block (title from the schema name, version from the schema version, falling back to 0.0.0), a top-level security: [{JWT: []}], and components.securitySchemes.JWT as HTTP bearer with bearerFormat: JWT.

PathOperations
/restpost, put, patch, delete — the bulk envelope
/rest/<entity>get (list), post (create), put, patch
/rest/<entity>/id/{id}get, put, patch, delete
/rest/<entity>/id/{id}/restorepost — soft-delete reversal
/x<endpoint path>one per scripted endpoint declared in the definitions

<entity> is the literal entity name: the document emits a concrete path set per entity (/rest/order, /rest/order/id/{id}, …), not one templated {entity} path. Scripted endpoints keep their declared path under the /x prefix, so path: /reports/sales emits /x/reports/sales.

List operations declare the query parameters limit, offset, cursor, expression (jq response shaping) and fields (comma-separated projection). Delete declares purge. Component schemas are the JSON Schema $defs with $refs rewritten into #/components/schemas/, plus two synthesised schemas: PageBlock (has_next_page, end_cursor) and ErrorResponse (error, code, details, with error and code required).

Emits $schema: https://json-schema.org/draft/2020-12/schema, a title (the schema name, or the entity name on the per-entity route), type: object, $defs, and a top-level version when the schema declares one. Every entity object sets additionalProperties: false. Named enums emit {type: string, enum: [...]} under their canonical PascalCase name; a field carrying an inline enum is referenced as <Entity><Field>, so status on order becomes OrderStatus.

The per-entity route emits only that entity’s three variants plus the enums its fields actually reference, so file-per-entity codegen does not pull in the whole document.

Field validations fold into schema keywords where an equivalent exists:

ValidationJSON SchemaZod
emailformat: email.email()
urlformat: uri.url()
ip (param version: v4 / v6)format: ipv4 / ipv6.ip()
regex (param expression)pattern.regex(/…/)
minmax (params min, max)minimum / maximum.min() / .max()
length (params min, max)minLength / maxLength.min() / .max()
enum, in (param values)enumz.enum([...])
phone.regex(…)
creditcard.refine(…)

Every other validation type is enforced by the engine but does not appear in any export. CUE expresses the same rules as lattice intersections (int & >=1 & <=1000, string & =~ "…"); it has no length keyword, so length becomes a ^.{min,max}$ regex.

Emits a package clause derived from the schema name (lowercased, with _, . and - folded to _), an import "time" when any datetime field exists, #<Enum> disjunctions, and the three structs per entity. Structs are open by default — they end with ... — so unknown fields validate; the renderer can close them for strict validation.

Emits syntax = "proto3", a package clause, an optional go_package option, one enum per schema enum, and three messages per entity. Enums gain a synthesised <PREFIX>_UNSPECIFIED = 0 and their values sorted alphabetically from 1.

Emits a JSON array of named types — enums first, then three records per entity. Enum symbols are sanitised to [A-Za-z_][A-Za-z0-9_]*. The record namespace is derived from the schema name by default and is configurable; because the namespace becomes the fully-qualified record name, it is also the schema-registry subject prefix, so set it deliberately rather than accepting the default.

Emits an edmx:Edmx Version="4.0" document with one Schema element, one EntityContainer (named Container by default), one EnumType per enum with members numbered from 0, one EntityType per entity keyed on its primary key, and one EntitySet per entity. Only the Output variant is emitted.

Emits asyncapi: "2.6.0", an info block, an optional servers.default entry, and three channels per entity — entity.<name>.created, entity.<name>.updated, entity.<name>.deleted — each with a subscribe operation whose operationId is on<Entity><Created|Updated|Deleted>. Message payloads carry id, type, source, subject, time, entity, operation, tenant, user_id and data, with id, type, time, entity, operation and data required. data refs the entity’s Output schema; input variants are skipped.

Discovery is how a client learns entity shape at runtime. Every route is GET, requires a bearer token, and accepts ?datastore=<name>.

RouteReturns
/data/schemaSchema summary — name, version, default_datastore, and sorted name lists for datastores, entities, relations, types, enums, traits, embeddables, stateflows, views, functions
/data/schema/graphqlThe generated GraphQL SDL as text/plain
/data/schema/entities{entities: [{name, fields, relations}], count}, sorted by name — fields and relations are counts
/data/schema/entities/{entity}Full detail for one entity
/data/stateflowsThe declared state machinesname, field, initial_state, states
/data/stateflow/{name}One state machine
/data/admin/data/actionsCatalogue of declared custom actions (admin policy gate)
/data/admin/data/references/{entity}Incoming and outgoing references for one entity (admin policy gate)
/data/admin/data/hashesThe named-query catalogue (admin policy gate)

The stateflow responses deliberately omit guards, actions, entry/exit hooks and conditions, because those reference scripts a caller should not see.

GET /data/schema/entities/{entity} is the shape call. Fields come back in declaration order:

{
"name": "order",
"table_name": "orders",
"primary_key": "id",
"materialization": "",
"fields": [
{
"name": "id",
"type": "",
"required": true,
"nullable": false,
"unique": true,
"primary_key": true,
"computed": false,
"default": "ulid()"
},
{
"name": "risk_band",
"type": "credit_score",
"required": false,
"nullable": true,
"unique": false,
"primary_key": false,
"computed": false,
"default": null
}
],
"relations": [
{
"name": "items",
"type": 2,
"to_entity": "order_item",
"from_field": "id",
"to_field": "order_id"
}
]
}
Terminal window
curl -H "Authorization: Bearer $TOKEN" https://api.example.com/data/schema/graphql
Terminal window
curl -H "Authorization: Bearer $TOKEN" https://api.example.com/data/schema/jsonschema/order.json

Exports and the SDL are rendered from the tenant’s resolved schema, which is cached per tenant and datastore. Loading new definitions does not by itself change what these routes return — the cached engine has to be dropped first.

Terminal window
curl -X DELETE -H "Authorization: Bearer $TOKEN" https://api.example.com/data/cache

The response is {"cleared": true, "tenant": "<tenant key>"}. Every cached resource for the calling tenant — schemas, pools, engines, action registries — is dropped and the next request rebuilds them.

CodeHTTPMeaning
v2.no_tenant403No tenant key on the request. The gateway did not inject the tenant headers, or the service was called directly without them.
v2.engine_unavailable500The tenant’s engine could not be resolved or built — datastore unreachable, or the definitions failed to load.
v2.missing_entity400The entity path segment was empty, e.g. a request to /data/schema/jsonschema/.
v2.entity_not_found404The named entity is not in the tenant’s resolved schema. Check the spelling and the ?datastore=.
v2.export_render_failed500A renderer returned an error while producing the document.
v2.cache_clear_failed500Cache invalidation failed while draining the tenant’s scopes.

The full external code table is on the block’s error reference, and the endpoint-level HTTP reference is at /api/data/.