CLI and library for working with UCP-annotated JSON Schemas.
UCP schemas use ucp_request and ucp_response annotations to define field visibility per operation. This tool resolves those annotations into standard JSON Schema, letting you validate payloads for specific operations (create, read, update, etc.).
# Install from crates.io
cargo install ucp-schema
# Or build from source
git clone https://github.com/Universal-Commerce-Protocol/ucp-schema
cd ucp-schema
cargo install --path .Given a UCP schema where id is omitted on create but required on update:
{
"type": "object",
"properties": {
"id": {
"type": "string",
"ucp_request": { "create": "omit", "update": "required" }
},
"name": { "type": "string" }
}
}Resolve it for different operations:
# For create: id is removed from the schema
ucp-schema resolve schema.json --request --op create --pretty
# For update: id is required
ucp-schema resolve schema.json --request --op update --prettyValidate a payload:
# This fails - id not allowed on create
echo '{"id": "123", "name": "test"}' > payload.json
ucp-schema validate payload.json --schema schema.json --request --op create
# This passes - id required on update
ucp-schema validate payload.json --schema schema.json --request --op updateucp-schema resolve <schema> --request|--response --op <operation> [options]
Options:
--pretty Pretty-print JSON output
--bundle Inline all external $ref pointers (see Bundling)
--strict=true Reject unknown fields (default: false, see Validation)
--output Write to file instead of stdoutExamples:
# Resolve for create request, pretty print
ucp-schema resolve checkout.json --request --op create --pretty
# Resolve for read response
ucp-schema resolve checkout.json --response --op read
# Resolve from URL
ucp-schema resolve https://ucp.dev/schemas/checkout.json --request --op create
# Save resolved schema to file
ucp-schema resolve checkout.json --request --op create --output resolved.jsonUCP payloads are self-describing: they embed capability metadata that declares which schemas apply. The validator can use this metadata directly, or you can specify an explicit schema.
# Self-describing mode (response: ucp.capabilities, JSONRPC request: meta.profile)
ucp-schema validate <payload> --op <operation> [options]
# REST request mode (profile via flag, payload is raw object)
ucp-schema validate <payload> --profile <profile> --op <operation> [options]
# Explicit schema mode (overrides self-describing)
ucp-schema validate <payload> --schema <schema> --request|--response --op <operation> [options]
Options:
--schema <path> Explicit schema (overrides self-describing mode)
--profile <path> Agent profile URL/path (REST request pattern)
--schema-local-base <dir> Local directory to resolve schema URLs (see Validation Modes)
--schema-remote-base <url> URL prefix to strip when mapping to local (see URL Prefix Mapping)
--request Direction is request (required with --schema, auto-detected otherwise)
--response Direction is response (required with --schema, auto-detected otherwise)
--json Output results as JSON (for automation)
--strict=true Reject unknown fields (default: false, see Validation)Exit codes:
0- Valid1- Validation failed (payload doesn't match schema)2- Schema error (invalid annotations, parse error, composition error)3- File/network error
The validator supports multiple modes based on which flags you provide:
| Mode | Command | Schema Source | Direction |
|---|---|---|---|
| Response (self-describing) | validate response.json --op read |
ucp.capabilities URLs |
Auto-detected |
| JSONRPC request | validate envelope.json --op create |
meta.profile URL |
Auto-detected |
| REST request | validate payload.json --profile profile.json --op create |
Explicit --profile URL |
Request |
| Explicit schema | validate payload.json --schema schema.json --request --op create |
Specified schema | Must specify |
Response Pattern (self-describing)
Response payloads embed capability metadata declaring which schemas apply:
{
"ucp": {
"capabilities": {
"dev.ucp.shopping.checkout": [{"version": "2026-01-26", "schema": "https://..."}]
}
},
"id": "checkout-123",
"line_items": [...]
}ucp-schema validate response.json --op readJSONRPC Request Pattern
JSONRPC requests have meta.profile at root, with payload nested under the capability short name:
{
"meta": {
"profile": "https://agent.example.com/.well-known/ucp"
},
"checkout": {
"line_items": [...]
}
}ucp-schema validate envelope.json --op createThe validator:
- Fetches the profile from
meta.profile - Extracts capabilities from the profile
- Extracts payload from the capability key (e.g.,
checkout) - Composes and validates
REST Request Pattern (--profile flag)
REST requests pass the profile via HTTP header (simulated with --profile), with payload being the raw object:
# Payload is just the checkout object (no envelope)
ucp-schema validate raw-checkout.json --profile agent-profile.json --op createThe --profile flag:
- Takes a profile URL or file path
- Extracts capabilities from the profile
- Treats the payload as the raw checkout object (no envelope extraction)
- Implies
--requestdirection
Mode 2: Self-describing + local resolution
Same as above, but schema URLs are resolved to local files instead of fetched:
# Schema URL https://ucp.dev/schemas/shopping/checkout.json
# Maps to: ./local/schemas/shopping/checkout.json
ucp-schema validate response.json --schema-local-base ./local --op readThe --schema-local-base flag maps URL paths to local files:
- URL:
https://ucp.dev/schemas/shopping/checkout.json - Path extracted:
/schemas/shopping/checkout.json - Local file:
{schema-local-base}/schemas/shopping/checkout.json
URL Prefix Mapping
When schema URLs have versioned prefixes that don't match your local directory structure, use --schema-remote-base to strip the prefix:
# Schema URL: https://ucp.dev/draft/schemas/shopping/checkout.json
# Local path: ./site/schemas/shopping/checkout.json (no "draft" directory)
ucp-schema validate response.json \
--schema-local-base ./site \
--schema-remote-base "https://ucp.dev/draft" \
--op readMapping with --schema-remote-base:
- URL:
https://ucp.dev/draft/schemas/shopping/checkout.json - Strip prefix:
https://ucp.dev/draft→/schemas/shopping/checkout.json - Local file:
{schema-local-base}/schemas/shopping/checkout.json
This is useful when published schemas have versioned $id URLs but your local files are organized without the version prefix.
Useful for: offline testing, local development, testing schema changes before deployment.
Mode 3: Explicit schema
Bypass self-describing metadata entirely by specifying --schema:
# Ignores any ucp.capabilities in payload, uses specified schema
ucp-schema validate order.json --schema checkout.json --request --op create
# Works with URLs too
ucp-schema validate order.json --schema https://ucp.dev/schemas/checkout.json --request --op createRequires: explicit --request or --response flag (direction cannot be auto-detected).
Error: No schema source
If payload has no ucp.capabilities/meta.profile AND no --schema/--profile is specified:
ucp-schema validate payload.json --op read
# Error: cannot infer direction: payload has no ucp.capabilities (response) or meta.profile (request)JSON output for automation:
ucp-schema validate order.json --schema checkout.json --request --op create --json
# Output: {"valid":true}
# Or: {"valid":false,"errors":[{"path":"","message":"..."}]}Catch schema errors before runtime. The linter checks for issues that would cause failures during resolution or validation.
ucp-schema lint <path> [options]
Options:
--format <text|json> Output format (default: text)
--strict Treat warnings as errors
--quiet, -q Only show errors, suppress progressWhat it checks:
| Category | Issue | Severity |
|---|---|---|
| Syntax | Invalid JSON | Error |
| References | $ref to missing file |
Error |
| References | $ref to missing anchor (e.g., #/$defs/foo) |
Error |
| Annotations | Invalid ucp_* type (must be string or object) |
Error |
| Annotations | Invalid visibility value (must be omit/required/optional) | Error |
| Hygiene | Missing $id field |
Warning |
| Hygiene | Unknown operation in annotation (e.g., {"delete": "omit"}) |
Warning |
Examples:
# Lint a directory of schemas
ucp-schema lint schemas/
# Lint single file, fail on warnings
ucp-schema lint checkout.json --strict
# CI-friendly JSON output
ucp-schema lint schemas/ --format json
# Quiet mode - only show errors
ucp-schema lint schemas/ --quietExit codes:
0- All files passed (or only warnings in non-strict mode)1- Errors found (or warnings in strict mode)2- Path not found
JSON output format:
{
"path": "schemas/",
"files_checked": 5,
"passed": 4,
"failed": 1,
"errors": 1,
"warnings": 2,
"results": [
{
"file": "checkout.json",
"status": "error",
"diagnostics": [
{
"severity": "error",
"code": "E002",
"path": "/properties/buyer/$ref",
"message": "file not found: types/buyer.json"
}
]
}
]
}UCP responses are self-describing - they embed ucp.capabilities declaring which schemas apply:
{
"ucp": {
"capabilities": {
"dev.ucp.shopping.checkout": [{
"version": "2026-01-11",
"schema": "https://ucp.dev/schemas/shopping/checkout.json"
}],
"dev.ucp.shopping.discount": [{
"version": "2026-01-11",
"schema": "https://ucp.dev/schemas/shopping/discount.json",
"extends": "dev.ucp.shopping.checkout"
}]
}
},
"id": "...",
"discounts": { ... }
}How composition works:
- Root capability: One capability has no
extends- this is the base schema - Extensions: Capabilities with
extendsadd fields to the root - Composition: Extensions define their additions in
$defs[root_capability_name] - allOf merge: The composed schema uses
allOfto combine all extensions
For the example above, the composed schema is:
{
"allOf": [
{ /* checkout's $defs["dev.ucp.shopping.checkout"] from discount.json */ }
]
}Schema authoring for extensions:
Extension schemas must define their additions in $defs under the root capability name:
{
"$id": "https://ucp.dev/schemas/shopping/discount.json",
"name": "dev.ucp.shopping.discount",
"$defs": {
"dev.ucp.shopping.checkout": {
"allOf": [
{ "$ref": "checkout.json" },
{
"type": "object",
"properties": {
"discounts": { /* discount-specific fields */ }
}
}
]
}
}
}Graph validation:
- Exactly one root capability (no
extends) - All
extendsreferences must exist in capabilities - All extensions must transitively reach the root (no orphan extensions)
UCP schemas often use $ref to reference external files:
{
"properties": {
"buyer": { "$ref": "types/buyer.json" },
"shipping": { "$ref": "types/address.json#/$defs/postal" }
}
}The --bundle flag inlines all external references, producing a self-contained schema:
ucp-schema resolve checkout.json --request --op create --bundle --prettyWhen to use bundling:
- Distributing schemas without file dependencies
- Feeding schemas to tools that don't support external refs
- Debugging to see the fully-expanded schema
- Pre-processing for faster repeated validation
How it works:
- External file refs (
"$ref": "types/buyer.json") are loaded and inlined - Fragment refs (
"$ref": "types/common.json#/$defs/address") navigate to the specific definition - Internal refs within external files (
"$ref": "#/$defs/foo") resolve correctly against their source file - Self-referential recursive types (
"$ref": "#") are preserved (can't be inlined) - Circular references between files are detected and reported as errors
By default, the validator respects UCP's extensibility model:
- Validates: Payload conforms to spec shape (types, required fields, enums, nested structures)
- Allows: Additional/unknown fields (extensibility is intentional)
# Validates that known fields are correct, allows extra fields
ucp-schema validate response.json --op readThis works because UCP schemas use additionalProperties: true intentionally - extensions add new fields, and forward compatibility requires tolerating unknown fields.
Enabling strict mode:
For cases where you want to reject unknown fields (e.g., closed systems, catching typos):
# Reject any fields not defined in schema
ucp-schema validate payload.json --schema schema.json --request --op create --strict=true
# Resolved schema will have additionalProperties: false injected
ucp-schema resolve schema.json --request --op create --strict=trueWhat strict mode does:
- Adds
additionalProperties: falseto all object schemas (root, nested, in arrays, in definitions) - Only injects
falsewhenadditionalPropertiesis missing or explicitlytrue - Preserves custom
additionalPropertiesschemas (e.g.,{"type": "string"}) - Preserves explicit
additionalProperties: false
Note: Strict mode does not work well with allOf composition (each branch validates independently and rejects properties from other branches). Use default non-strict mode for composed schemas.
Annotations control how fields appear in the resolved schema:
| Value | Effect on Properties | Effect on Required Array |
|---|---|---|
"omit" |
Field removed | Field removed |
"required" |
Field kept | Field added |
"optional" |
Field kept | Field removed |
| (no annotation) | Field kept | Unchanged |
Shorthand - applies to all operations:
{ "ucp_request": "omit" }Per-operation - different behavior per operation:
{ "ucp_request": { "create": "omit", "update": "required", "read": "omit" } }Separate request/response:
{
"ucp_request": { "create": "omit" },
"ucp_response": "required"
}See FAQ.md for common questions about validator behavior and design decisions
Apache-2.0