Skip to content

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:

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 SlotDescriptor objects — 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:

cd VDP
podman run --rm -v .:/work:Z -w /work node:lts node tests/run-transforms.mjs