ucp-schema
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.).
Installation
# Install from crates.io
# Or build from source
Quick Start
Given a UCP schema where id is omitted on create but required on update:
Resolve it for different operations:
# For create: id is removed from the schema
# For update: id is required
Validate a payload:
# This fails - id not allowed on create
# This passes - id required on update
CLI Reference
resolve - Generate operation-specific schema
|
)
--strict=true )
Examples:
# Resolve for create request, pretty print
# Resolve for read response
# Resolve from URL
# Save resolved schema to file
validate - Validate payload against resolved schema
UCP 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 (extracts schema from payload's ucp.capabilities)
# Explicit schema mode (overrides self-describing)
|
)
)
)
)
)
)
--strict=true )
Exit codes:
0- Valid1- Validation failed (payload doesn't match schema)2- Schema error (invalid annotations, parse error, composition error)3- File/network error
Validation Modes
The validator supports three modes based on which flags you provide:
| Mode | Command | Schema Source | Direction |
|---|---|---|---|
| Self-describing + remote | validate payload.json --op read |
ucp.capabilities URLs fetched |
Auto-detected |
| Self-describing + local | validate payload.json --schema-local-base ./dir --op read |
ucp.capabilities URLs mapped to local files |
Auto-detected |
| Explicit schema | validate payload.json --schema schema.json --request --op create |
Specified schema file/URL | Must specify --request or --response |
Mode 1: Self-describing + remote fetch
UCP payloads embed capability metadata declaring which schemas apply. The validator extracts schema URLs and fetches them:
# Payload has ucp.capabilities with schema URLs like https://ucp.dev/schemas/...
# Validator fetches schemas from those URLs and composes them
Requires: payload has ucp.capabilities (responses) or ucp.meta.profile (requests).
Direction is auto-detected from payload structure.
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
The --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)
Mapping 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
# Works with URLs too
Requires: explicit --request or --response flag (direction cannot be auto-detected).
Error: No schema source
If payload has no ucp.capabilities/ucp.meta.profile AND no --schema is specified:
# Error: payload is not self-describing: missing ucp.capabilities and ucp.meta.profile
JSON output for automation:
# Output: {"valid":true}
# Or: {"valid":false,"errors":[{"path":"","message":"..."}]}
lint - Static analysis of schema files
Catch schema errors before runtime. The linter checks for issues that would cause failures during resolution or validation.
|)
What 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
# Lint single file, fail on warnings
# CI-friendly JSON output
# Quiet mode - only show errors
Exit 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:
Schema Composition from Capabilities
UCP responses are self-describing - they embed ucp.capabilities declaring which schemas apply:
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:
Schema authoring for extensions:
Extension schemas must define their additions in $defs under the root capability name:
Graph validation:
- Exactly one root capability (no
extends) - All
extendsreferences must exist in capabilities - All extensions must transitively reach the root (no orphan extensions)
Bundling External References
UCP schemas often use $ref to reference external files:
The --bundle flag inlines all external references, producing a self-contained schema:
When 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
Validation
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
This 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
# Resolved schema will have additionalProperties: false injected
What 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.
Visibility Rules
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 |
Annotation Formats
Shorthand - applies to all operations:
Per-operation - different behavior per operation:
Separate request/response:
More Information
See FAQ.md for common questions about validator behavior and design decisions
License
MIT