The validator checks that specced fields are correct, but allows additional fields by design.
This is intentional:
- The schema defines minimum requirements, not an exhaustive contract
- Extensions add fields that base schemas don't know about
- Forward compatibility requires tolerating unknown fields
- Clients shouldn't break when servers add new fields
If a payload has { "id": "123", "custom_field": "foo" } and the schema only defines id, validation passes. The custom_field is ignored—not rejected.
Use --strict only when you need a closed contract:
- Catching field name typos during development
- Systems where the schema is the complete specification
They're different validation modes:
| Flag | Mode | Schema source |
|---|---|---|
| (none) | Self-describing | Fetches URLs from ucp.capabilities |
--schema-local-base ./dir |
Self-describing + local | Maps capability URLs to local files |
--schema file.json |
Explicit | Uses specified schema, ignores capabilities |
--schema-local-base is useful for:
- Offline testing
- Local development before publishing schemas
- Testing schema changes against real payloads
How it works: The flag extracts the URL path and maps it to a local file. This works for any domain—not just ucp.dev:
| Schema URL | Local path (--schema-local-base ./local) |
|---|---|
https://ucp.dev/schemas/shopping/checkout.json |
./local/schemas/shopping/checkout.json |
https://extensions.3p.com/schemas/loyalty.json |
./local/schemas/loyalty.json |
This means you can develop and test third-party extensions locally before publishing.
The validator infers direction from payload structure:
| Payload has | Detected direction |
|---|---|
ucp.capabilities |
Response |
meta.profile |
Request |
| Neither | Error (must specify --request or --response) |
This applies to both validate and resolve when the input is a self-describing payload. When resolving a plain schema file, explicit --request or --response is required.
The validator fails fast on invalid annotations. Valid values are: "omit", "required", "optional".
"id": { "ucp_request": "readonly" } // Error: unknown visibilityTypos and version mismatches should surface immediately, not silently degrade to "include everything" behavior. If you see this error, either fix the typo or update your tooling.
Omit means "don't send this"—not just "we won't validate it."
When a schema has additionalProperties: false and a field is omitted:
{
"additionalProperties": false,
"properties": {
"id": { "ucp_request": "omit" },
"name": { "type": "string" }
}
}Sending { "name": "foo", "id": "123" } for a request fails. The id field was removed from properties, making it an "additional property" that gets rejected.
Why: If the server generates id, clients shouldn't send it. The schema enforces this contract.
Extensions must define their additions in $defs[root_capability_name]. Composition happens at validation time. Each extension owns its additions but references the base it extends.
If dev.ucp.shopping.checkout is the root capability, your extension schema should look like:
{
"$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": { ... }
}
}
]
}
}
}The validator:
- Finds the root capability (no
extends) - Extracts
$defs[root_name]from each extension - Composes them with
allOf
No. Extensions can tighten requirements, not loosen them.
This is JSON Schema semantics. With allOf, ALL branches must validate:
| Base | Extension | Result |
|---|---|---|
| omit | required | required |
| optional | required | required |
| required | omit | required (base wins) |
| required | optional | required (base wins) |
If the base schema says id is required, clients already depend on it. An extension can't hide it without breaking those clients. Extensions add requirements; they don't remove them.