Schema Reference¶
The VDP JSON Schemas define the structure of view descriptor documents and the discovery document. Both use the JSON Schema 2020-12 dialect.
Current versions, published at their canonical $id URLs:
vdp.v0-2.schema.json— validates standalone view descriptors and multi-view descriptors, including the v0.2transformmembervdp-discovery.v0-2.schema.json— validates the discovery document served at/.well-known/vdp
Previous versions — superseded by v0.2 but kept published so their $id URLs continue to resolve: vdp.v0-1.schema.json, vdp-discovery.v0-1.schema.json.
Because the schemas are hosted at their $id URLs, they can be referenced directly from other schemas and validators.
View Descriptor Schema¶
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://vdprotocol.org/schemas/vdp.v0-2.schema.json",
"title": "View Descriptor Protocol (VDP) v0.2",
"description": "Schema for VDP view descriptor documents. Validates standalone ViewDescriptor and MultiViewDescriptor payloads, including the v0.2 transform member (spec Section 3.8). Definitions in $defs can be referenced by other schemas for inline body transport (_view / _views). Unrecognized members are rejected unless prefixed with x- (spec Section 3.10). The discovery document served at /.well-known/vdp has its own schema: vdp-discovery.v0-2.schema.json.",
"oneOf": [
{ "$ref": "#/$defs/ViewDescriptor" },
{ "$ref": "#/$defs/MultiViewDescriptor" }
],
"$defs": {
"ViewDescriptor": {
"type": "object",
"description": "A view descriptor identifying a root template URI, its dynamic slot assignments, and optionally a transform adapting the response representation to the template's model. Slots are recursive — each slot value is a ViewDescriptor, a DescriptorReference, or an array of either.",
"properties": {
"template": {
"$ref": "#/$defs/TemplateURI"
},
"type": {
"type": "string",
"minLength": 1,
"description": "Advisory media type (RFC 6838) of the template resource, e.g. text/x-qute. The Content-Type of the fetched template is authoritative."
},
"integrity": {
"type": "string",
"minLength": 1,
"description": "Integrity metadata for the template resource in W3C Subresource Integrity format, e.g. sha384-<base64 digest>. A mismatch is treated as a template fetch failure."
},
"slots": {
"$ref": "#/$defs/Slots"
},
"transform": {
"$ref": "#/$defs/Transform"
}
},
"required": ["template"],
"patternProperties": { "^x-": true },
"unevaluatedProperties": false
},
"DescriptorReference": {
"type": "object",
"description": "A reference to a standalone view descriptor resource, valid only as a slot value. The client fetches the URL and uses the result as the slot's descriptor. The URL may be a relative reference, resolved against the same base as the containing descriptor's template URIs. A reference site carries no transform (spec Section 3.7).",
"properties": {
"descriptor": {
"type": "string",
"format": "uri-reference",
"minLength": 1,
"description": "URL of the referenced view descriptor resource."
}
},
"required": ["descriptor"],
"patternProperties": { "^x-": true },
"unevaluatedProperties": false
},
"TemplateURI": {
"type": "string",
"format": "uri-reference",
"minLength": 1,
"description": "A URI identifying a template. The URI is an identifier first — which source supplies the template (app bundle, page, BFF-local store, network fetch) is deployment-specific and outside the protocol. The scheme is optional: a scheme-less identifier that does not begin with '/' (e.g. example.com/templates/card) is an opaque, host-qualified identity and is NOT resolved as an RFC 3986 relative reference (spec Section 5.4); the resolving code supplies a scheme only when it fetches over the network. Whenever a template is retrieved over the network, retrieval MUST use HTTPS (loopback addresses excepted for local development)."
},
"Slots": {
"type": "object",
"description": "A map of slot names to slot values. Each key is a named insertion point in the parent template. Each value is a SlotDescriptor (an inline ViewDescriptor or a DescriptorReference) or an array of SlotDescriptors rendered in sequence.",
"additionalProperties": {
"$ref": "#/$defs/SlotValue"
}
},
"SlotDescriptor": {
"description": "An inline ViewDescriptor or a DescriptorReference pointing at a standalone view descriptor resource.",
"oneOf": [
{ "$ref": "#/$defs/ViewDescriptor" },
{ "$ref": "#/$defs/DescriptorReference" }
]
},
"SlotValue": {
"description": "A single SlotDescriptor or an ordered array of SlotDescriptors. Arrays are rendered in sequence within the slot.",
"oneOf": [
{ "$ref": "#/$defs/SlotDescriptor" },
{
"type": "array",
"items": {
"$ref": "#/$defs/SlotDescriptor"
},
"minItems": 1
}
]
},
"MultiViewDescriptor": {
"type": "object",
"description": "Multiple named view descriptors for a single API response. Clients SHOULD use the 'default' view when no specific view is requested.",
"properties": {
"views": {
"type": "object",
"description": "A map of view names to ViewDescriptors.",
"additionalProperties": {
"$ref": "#/$defs/ViewDescriptor"
},
"minProperties": 1
}
},
"required": ["views"],
"patternProperties": { "^x-": true },
"unevaluatedProperties": false
},
"JsonPointer": {
"type": "string",
"pattern": "^(/([^/~]|~[01])*)*$",
"description": "An RFC 6901 JSON Pointer. The empty string addresses the whole transform input. A pointer that resolves to nothing yields null (spec Section 3.8.2) — that is not an error."
},
"Transform": {
"description": "A transform (spec Section 3.8): a JSON Pointer, a Mapping, or a MapperRef. Evaluated against the original response representation (with any embedded _view/_views removed), never against an ancestor node's transform output.",
"if": { "type": "object", "required": ["$mapper"] },
"then": { "$ref": "#/$defs/TransformMapperRef" },
"else": { "$ref": "#/$defs/TransformNode" }
},
"TransformNode": {
"description": "A transform node: Pointer | Mapping | List | Projection | Entries | Defaulted | Count | Merge. Discriminated on key presence with if/then/else so validation errors name the intended construct.",
"if": { "type": "object", "required": ["$map"] },
"then": { "$ref": "#/$defs/TransformProjection" },
"else": {
"if": { "type": "object", "required": ["$entries"] },
"then": { "$ref": "#/$defs/TransformEntries" },
"else": {
"if": { "type": "object", "required": ["$get"] },
"then": { "$ref": "#/$defs/TransformDefaulted" },
"else": {
"if": { "type": "object", "required": ["$count"] },
"then": { "$ref": "#/$defs/TransformCount" },
"else": {
"if": { "type": "object", "required": ["$merge"] },
"then": { "$ref": "#/$defs/TransformMerge" },
"else": {
"if": { "type": "string" },
"then": { "$ref": "#/$defs/JsonPointer" },
"else": {
"if": { "type": "array" },
"then": { "$ref": "#/$defs/TransformList" },
"else": { "$ref": "#/$defs/TransformMapping" }
}
}
}
}
}
}
},
"TransformMapping": {
"type": "object",
"description": "An output object: each key becomes a member of the produced model, each value is a transform node evaluated against the same input. Keys MUST NOT begin with '$' (reserved for constructs); an unrecognized '$'-prefixed member makes the descriptor invalid (spec Section 9.3). {} is legal and produces {}.",
"propertyNames": { "pattern": "^(?!\\$)" },
"additionalProperties": { "$ref": "#/$defs/TransformNode" }
},
"TransformList": {
"type": "array",
"description": "An output array built from transform nodes, each evaluated against the same input.",
"items": { "$ref": "#/$defs/TransformNode" }
},
"TransformProjection": {
"type": "object",
"description": "Projects each element of the array at $map through $to. Inside $to, pointers are relative to the current element. A non-array $map target yields null (not an error).",
"properties": {
"$map": { "$ref": "#/$defs/JsonPointer" },
"$to": { "$ref": "#/$defs/TransformNode" }
},
"required": ["$map", "$to"],
"patternProperties": { "^x-": true },
"unevaluatedProperties": false
},
"TransformEntries": {
"type": "object",
"description": "Emits {\"key\": ..., \"value\": ...} per member of the object at $entries, in document order (spec Section 3.8.2), optionally reshaped through $to. A non-object target yields null.",
"properties": {
"$entries": { "$ref": "#/$defs/JsonPointer" },
"$to": { "$ref": "#/$defs/TransformNode" }
},
"required": ["$entries"],
"patternProperties": { "^x-": true },
"unevaluatedProperties": false
},
"TransformDefaulted": {
"type": "object",
"description": "The value at $get, or the literal $default when the pointer resolves to nothing (or to an explicit null — the two are indistinguishable, spec Section 3.8.2).",
"properties": {
"$get": { "$ref": "#/$defs/JsonPointer" },
"$default": true
},
"required": ["$get", "$default"],
"patternProperties": { "^x-": true },
"unevaluatedProperties": false
},
"TransformCount": {
"type": "object",
"description": "The element count of the array at $count, or the member count of the object at $count; null for anything else.",
"properties": {
"$count": { "$ref": "#/$defs/JsonPointer" }
},
"required": ["$count"],
"patternProperties": { "^x-": true },
"unevaluatedProperties": false
},
"TransformMerge": {
"type": "object",
"description": "Shallow-merges the object results of the operand nodes; last operand wins on key collision. Operands that do not evaluate to objects (including null) are skipped.",
"properties": {
"$merge": {
"type": "array",
"items": { "$ref": "#/$defs/TransformNode" },
"minItems": 1
}
},
"required": ["$merge"],
"patternProperties": { "^x-": true },
"unevaluatedProperties": false
},
"TransformMapperRef": {
"type": "object",
"description": "A reference to mapping code the client has registered (spec Section 3.8.3). The URI is an identifier, matched verbatim, never fetched — the same identity rules as template URIs. Valid only as the entire transform value. Client support is OPTIONAL; an unrecognized mapper URI is a slot failure (spec Section 9.1).",
"properties": {
"$mapper": {
"type": "string",
"format": "uri-reference",
"minLength": 1
}
},
"required": ["$mapper"],
"patternProperties": { "^x-": true },
"unevaluatedProperties": false
}
}
}
The schema validates descriptor shape — it cannot verify that a transform's output matches what the template expects. A transform producing {"heading": ...} for a template wanting {"title": ...} is schema-valid and renders blank. A natural future direction is template-declared model schemas published at the template URI, validated once at cache-warm time.
Discovery Document Schema¶
The discovery document is not a view descriptor — it is served as application/vdp-discovery+json (never application/vdp+json) and has its own schema:
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://vdprotocol.org/schemas/vdp-discovery.v0-2.schema.json",
"title": "View Descriptor Protocol (VDP) v0.2 Discovery Document",
"description": "Schema for the VDP discovery document served at /.well-known/vdp as application/vdp-discovery+json (spec Section 13.2). Unrecognized members are permitted everywhere per the discovery extensibility clause: clients MUST ignore members they do not recognize.",
"type": "object",
"properties": {
"version": {
"type": "string",
"description": "The VDP protocol version supported by the API (e.g. \"0.2\"). Matches the value advertised by the VDP-Version header.",
"minLength": 1
},
"endpoints": {
"type": "object",
"description": "Maps API paths to their view descriptor resources. Keys are absolute paths relative to the origin serving the discovery document, and MAY be RFC 6570 Level 1 URI Templates (e.g. /api/products/{id}). Entries are a prefetch hint for the default representation; the descriptor delivered with a response is authoritative (spec Section 4.4).",
"propertyNames": {
"pattern": "^/"
},
"additionalProperties": {
"$ref": "#/$defs/EndpointEntry"
}
},
"trustedTemplateUrls": {
"type": "array",
"description": "Template URI allowlist (spec Section 10). Each entry is a URL prefix; entries SHOULD end with a trailing slash.",
"items": {
"type": "string",
"format": "uri-reference",
"minLength": 1
}
},
"mappers": {
"type": "array",
"description": "The $mapper URIs (spec Section 3.8.3) that descriptors from this API may reference. Mapper URIs are identifiers, matched verbatim against the client's registered mappers — listing one does not make it fetchable.",
"items": {
"type": "string",
"format": "uri-reference",
"minLength": 1
}
}
},
"required": ["version"],
"$defs": {
"EndpointEntry": {
"type": "object",
"description": "Discovery metadata for one API endpoint. Additional members are permitted for extensibility.",
"properties": {
"descriptor": {
"type": "string",
"format": "uri-reference",
"description": "URL of the endpoint's view descriptor resource. MAY be a relative reference, resolved against the discovery document URL per RFC 3986.",
"minLength": 1
}
},
"required": ["descriptor"]
}
}
}
Type Reference¶
ViewDescriptor¶
The core type. Identifies a root template and optionally declares slot assignments, a transform, and advisory template metadata. Unrecognized members are rejected unless their names begin with x- (Specification Section 3.10).
| Property | Type | Required | Description |
|---|---|---|---|
template |
TemplateURI |
Yes | URI identifying the template resource |
type |
string |
No | Advisory media type (RFC 6838) of the template resource; the fetched template's Content-Type is authoritative |
integrity |
string |
No | W3C Subresource Integrity metadata for the template resource; a mismatch is treated as a template fetch failure |
slots |
Slots |
No | Map of slot names to slot values |
transform |
Transform |
No | Declarative mapping from the response representation to the template's model (Specification Section 3.8) |
TemplateURI¶
A URI-reference string (format: "uri-reference") identifying a template. The URI is an identifier first — which source supplies the template (app bundle, page-shipped templates, BFF-local store, network fetch) is deployment-specific and outside the protocol (Specification Section 6.3). The scheme is optional: a scheme-less identifier that does not begin with / (e.g. example.com/templates/card) is an opaque, host-qualified identity and is NOT resolved as an RFC 3986 relative reference (Specification Section 5.4); the resolving code supplies a scheme only when it fetches over the network. Whenever a template is retrieved over the network, retrieval MUST use HTTPS in production.
Slots¶
An object where each key is a slot name (matching an insertion point in the parent template) and each value is a SlotValue.
SlotValue¶
One of:
- A single
SlotDescriptor— one template (or referenced descriptor) fills the slot - An array of
SlotDescriptorobjects — multiple entries rendered in sequence within the slot (minimum 1 item)
SlotDescriptor¶
Either an inline ViewDescriptor or a DescriptorReference.
DescriptorReference¶
A reference to a standalone view descriptor resource, valid only as a slot value (never at the root). The client fetches the URL and uses the result as the slot's descriptor. A reference site carries no transform, and the referenced descriptor SHOULD NOT contain one (Specification Section 3.7).
| Property | Type | Required | Description |
|---|---|---|---|
descriptor |
string (uri-reference) |
Yes | URL of the referenced view descriptor resource; may be relative, resolved against the same base as the containing descriptor's template URIs |
MultiViewDescriptor¶
Wraps multiple named views for a single API response.
| Property | Type | Required | Description |
|---|---|---|---|
views |
object |
Yes | Map of view names to ViewDescriptor objects (minimum 1 entry) |
Clients SHOULD use the default view when no specific view is requested.
Transform¶
A declarative mapping evaluated against the original response representation (Specification Section 3.8). One of:
- A JSON Pointer string (RFC 6901;
""selects the whole input) - A Mapping object — each key becomes an output member, each value is a nested transform node; keys MUST NOT begin with
$ - A MapperRef —
{"$mapper": "<uri>"}naming client-registered mapping code (identifier only, never fetched; valid only as the entire transform value)
Inside a mapping (or list), a node may also be one of the $-constructs:
| Construct | Shape | Result |
|---|---|---|
| Projection | {"$map": <pointer>, "$to": <node>} |
Each element of the array at $map, reshaped through $to (pointers inside $to are element-relative); null if the target is not an array |
| Entries | {"$entries": <pointer>, "$to"?: <node>} |
One {"key", "value"} pair per object member, in document order; null if the target is not an object |
| Defaulted | {"$get": <pointer>, "$default": <any>} |
The value at $get, or the $default literal when the pointer resolves to nothing |
| Count | {"$count": <pointer>} |
Array element count or object member count; null otherwise |
| Merge | {"$merge": [<node>, ...]} |
Shallow merge of the object results, last wins; non-object results are skipped |
An unrecognized $-prefixed member makes the descriptor invalid (Specification Section 9.3). Missing pointers yield null and are not errors.
Discovery Document¶
Served at /.well-known/vdp as application/vdp-discovery+json (Specification Sections 12.3 and 13.2). Unrecognized members anywhere in the document MUST be ignored by clients.
| Property | Type | Required | Description |
|---|---|---|---|
version |
string |
Yes | VDP protocol version supported by the API |
endpoints |
object |
No | Map of API paths to { "descriptor": <url> } entries. Keys are absolute paths relative to the origin serving the discovery document and MAY be RFC 6570 Level 1 URI Templates (e.g. /api/products/{id}). descriptor values MAY be relative references, resolved against the discovery document URL |
trustedTemplateUrls |
string[] |
No | Template URI allowlist — trusted URL prefixes; entries SHOULD end with a trailing slash |
mappers |
string[] |
No | The $mapper URIs that descriptors from this API may reference (Specification Section 3.8.3); identifiers matched verbatim, never fetched |
Validation¶
The view descriptor schema validates standalone VDP documents (view descriptor JSON files). For inline transport (_view / _views embedded in API responses), reference the $defs types from your own API schema:
{
"properties": {
"_view": { "$ref": "https://vdprotocol.org/schemas/vdp.v0-2.schema.json#/$defs/ViewDescriptor" }
}
}
To validate the VDP examples locally:
cd VDP
podman run --rm -v .:/work:Z -w /work node:lts sh -c \
"npm install --no-save ajv-cli ajv-formats && \
npx ajv-cli test --spec=draft2020 -s vdp.v0-2.schema.json -d 'examples/vdp-*.json' --valid -c ajv-formats && \
npx ajv-cli test --spec=draft2020 -s vdp-discovery.v0-2.schema.json -d 'examples/discovery-*.json' --valid -c ajv-formats"
The VDP repository also ships a language-neutral test corpus (tests/) with transform evaluation fixtures and a dependency-free reference runner: