chore: Refactor schema references to common types#436
Conversation
- Added a new common schemas directory and updated references in various shopping schemas to point to common types. - Removed shopping types such as amount.json, buyer.json, and others, consolidating their definitions into the common directory. - Adjusted paths in multiple JSON schema files to ensure they correctly reference the new common types, enhancing maintainability and consistency across the project.
…string, ensuring correct processing of fragment-stripped paths for JSON schema references.
jamesandersen
left a comment
There was a problem hiding this comment.
thanks @amithanda for setting these types up for more broad use
… were mistakenly removed
|
TY for restoring error codes, that was my main flag on firt pass. Pedantic nice: technically this change may be breaking because we're changing $id pointers and URLs from published schemas. If any code or validators cached that state, they'll get 404's. My intuition is that we should be OK, but this is something to watch out for... let's see if and what breaks. Question: how do we want to distinguish, or not, common vs domain-specific schemas? Right now they're indistinguishable in how we render then in our tables. Should we separate and note that some are common vs not? This also connects to my flag on |
buyer carries shopping-domain semantics — it implies a purchase transaction and may not generalize cleanly to non-shopping verticals. Promoting it to common/ would lock in shopping-centric naming before cross-vertical requirements are known. A neutral common type (e.g. party or contact) should be introduced when the first non-shopping vertical is designed, with field names validated against actual cross-vertical needs rather than inferred from shopping.
… build time Adds _validate_common_vertical_uniqueness() to main.py, which runs at module load time and fails fast if any filename in common/types/ collides with a filename in a vertical's types/ directory (currently shopping/types/). A common/vertical collision is always a design mistake: the docs macro system resolves schemas by basename across SCHEMAS_DIRS, so a collision silently picks the wrong schema — producing incorrect anchors and broken reference links with no build error. Two verticals sharing a filename (e.g. shopping/checkout.json and newvertical/checkout.json) is intentional and is deliberately not flagged. New verticals are registered by adding one line to VERTICAL_TYPES_DIRS.
You're right that the reference page currently renders all types indistinguishably: Shadow/duplicate schema guardImplemented — but we scoped the guard more narrowly than your suggestion after thinking through the |
|
Following up on @yanheChen and @jingyli's comments — agreed that Reverted
The neutral cross-vertical person type is tracked as a follow-up for when the first non-shopping |
Replaces all inline Path(...) literals in SCHEMAS_DIRS and related
configuration with named constants, making the directory hierarchy
explicit and eliminating duplication:
SCHEMAS_DIR = Path("source/schemas")
HANDLERS_GOOGLE_PAY_DIR = Path("source/handlers/google_pay")
SHOPPING_SCHEMAS_DIR = SCHEMAS_DIR / "shopping"
SHOPPING_TYPES_DIR = SHOPPING_SCHEMAS_DIR / "types"
COMMON_SCHEMAS_DIR = SCHEMAS_DIR / "common"
COMMON_TYPES_DIR = COMMON_SCHEMAS_DIR / "types"
SCHEMAS_DIRS and VERTICAL_TYPES_DIRS now reference only named constants.
UCP_SCHEMA_PATH derives from SCHEMAS_DIR rather than repeating the
literal path. No behavioral changes.
The redirect logic in create_link still resolved schema paths through raw string literals after the named-constants refactor extracted SCHEMAS_DIR / COMMON_TYPES_DIR / SHOPPING_TYPES_DIR / SHOPPING_SCHEMAS_DIR. Consolidate on the constants.
The previous guard required VERTICAL_TYPES_DIRS to be manually maintained
("Add new vertical types directories here as verticals are introduced")
and only enforced uniqueness between common/types/ and the listed vertical
type dirs.
Auto-discover non-common immediate subdirs of source/schemas/ as verticals
and extend the guard to cover every pair of type namespaces (common ×
vertical and vertical × vertical). Cost: verticals cannot share type
filenames; the escape hatch ("rename or refactor the resolver to be
path-aware") is surfaced in the error message when someone hits it.
Auto-discovery also surfaced source/schemas/transports/ as a sibling
namespace not previously tracked. It has no types/ subdir today so the
guard is a no-op for it, but it will participate automatically if one
appears.
igrigorik
left a comment
There was a problem hiding this comment.
@amithanda pushed a small improvement to the validation logic, please sanity check.
LGTM.
@igrigorik - Thanks for the cleanup and auto-discovery in However, zooming out to the multi-vertical design, there is a fundamental gap in enforcing global filename uniqueness across all vertical namespaces:
Proposal: A Path-Aware ResolverInstead of using strict filename constraints to workaround the resolver's flat-search design, we should make the resolver path-aware. We can inspect the MkDocs compilation context ( Here is a quick sketch of how we can achieve this in def get_active_vertical() -> str:
try:
# src_path is like "specification/new-vertical/checkout.md"
# or "specification/checkout.md" (defaults to shopping)
src_path = Path(env.page.file.src_path)
parts = src_path.parts
if len(parts) > 2 and parts[0] == "specification":
vertical = parts[1]
# Verify against discovered verticals
if vertical in [d.name for d in VERTICAL_DIRS]:
return vertical
except AttributeError:
pass
return "shopping" # Default vertical fallbackThen, in def _load_json_file(entity_name):
"""Try loading a JSON file prioritizing the active vertical."""
active_vertical = get_active_vertical()
# Construct prioritized search paths for the active vertical
active_schemas_dir = SCHEMAS_DIR / active_vertical
active_types_dir = active_schemas_dir / "types"
search_dirs = []
if active_types_dir.exists():
search_dirs.append(active_types_dir)
if active_schemas_dir.exists():
search_dirs.append(active_schemas_dir)
# Append remaining directories in standard order
for d in schemas_dirs:
if d not in search_dirs:
search_dirs.append(d)
# Resolve file
for schemas_dir in search_dirs:
full_path = Path(schemas_dir) / (entity_name + ".json")
try:
with full_path.open(encoding="utf-8") as f:
return json.load(f)
except FileNotFoundError:
continue
return NoneNext StepsIf this looks good, we can:
OR We can come back and look at build time enforcement once we have some idea on the patterns that are emerging. |
Right, I see this as a feature, not a bug. If we promote something to common, then let's not create a shadow override in sub-verticals -- that violates the contract of common. |
I think there are 3 issues we should discuss:1. How should we generate documentation clearly for each vertical? (Path-Aware Resolver)
Since
2. Should we enforce that vertical and common namespaces cannot have overlapping names?
3. Should we enforce that filenames be unique between different verticals?
Once the Path-Aware Resolver (Point 1) is implemented, the documentation build will resolve vertical-specific files correctly based on the compilation context. Sibling verticals should have the freedom to design their namespaces independently using clean, intuitive terms. |
The previous validator (72eca50) overreached in two directions: - only checked types/ subdirs, missing top-level capability schema collisions (e.g. newthing/checkout.json shadowing shopping/checkout.json would not have triggered); - enforced vertical-vs-vertical uniqueness, restricting vertical autonomy without justification. The actual invariant — confirmed in TC on 2026-05-15 — is narrower: once a basename is claimed by common/ (top-level or types/), no vertical may use that filename. Verticals are autonomous siblings and may share filenames with each other; the docs resolver's path ambiguity for vertical-vs-vertical is a separate follow-up tracked as the path-aware resolver work.
|
@amithanda updated per our TC discussion, PTAL -- right shape? |
Upstream main moved 15 widely-shared types from source/schemas/shopping/types/ to source/schemas/common/types/ (#436) to enable cross-vertical reuse — most recently the loyalty extension's common/loyalty.json depends on the relocated amount.json. Update every `schema=shopping/types/error_response` annotation on our branch to `schema=common/types/error_response` so the validator can resolve the schema at its new location. 31 annotations across 14 spec docs; pure path swap, no semantic change.
* feat: schema-validated documentation examples
Introduce a validation harness that ensures every JSON example in the spec
docs is either validated against UCP schemas or explicitly skipped. This
catches documentation drift — when schemas change, stale examples break CI
instead of silently misleading readers.
## Contract
Every ```json block requires an annotation on the preceding line:
<!-- ucp:example schema=shopping/checkout op=read -->
<!-- ucp:example schema=shopping/checkout path=$.totals op=read -->
<!-- ucp:example schema=shopping/cart op=create direction=request -->
<!-- ucp:example skip reason="JSON-RPC transport binding" -->
Unannotated blocks are hard failures.
Principle: "if you open it, you own it." When an example opens an object's
braces, every required field (per the resolved schema for the declared
op + direction) must be shown or acknowledged. This applies recursively.
Three-tier value contract for annotated (non-skip) examples:
- Full value: validated against schema (types, constraints, enums)
- Required fields ({ ... }, [ ... ], "..."): field must exists but
values are optional; scaffold fills it for validation.
- Absent fields only permitted for optional schema fields
## Pipeline
1. Extract — find annotated ```json blocks (handles tab indentation)
2. Unwrap — strip HTTP headers from request/response examples
3. Expand — substitute {{ ucp_version }} with valid date
4. Preprocess — convert bare ... to valid JSON ({ ... } → {"...":"..."})
5. Coverage — walk resolved schema, verify required fields acknowledged
6. Strip — remove ellipsis markers
7. Merge — deep-merge into scaffold (example wins, scaffold fills gaps)
8. Validate — ucp-schema validate on merged payload
9. Report — map errors to source file:line
## Bugs caught and fixed
- checkout.md: item missing required `price` field in disclosure example
- checkout.md: totals array missing required `subtotal` entry
- checkout-rest.md: business outcome used non-schema `available_quantity`
field on line_item; replaced with message-based pattern
- discount.md (4 instances): `quantity` wrongly nested inside `item`
object instead of on `line_item` — schema requires quantity at line_item
level
- discount.md: `price` and `title` shown in create requests where schema
omits them (request-only fields: only `item.id` is valid)
## Current state
268 blocks across 39 files:
- 52 validated (REST request/response pairs, core spec examples, error
responses, extension examples)
- 216 skipped (transport bindings, schema definitions, configs — future
work: payload_path= for JSON-RPC unwrap)
Run: python3 scripts/validate_examples.py --schema-base source/schemas/
* fix: validate empty request bodies instead of skipping
Address review feedback (drewolson-google): empty `{}` request bodies
for GET and cancel operations are valid requests, not skip-worthy.
The harness now recognizes `{}` as trivially valid — no coverage or
schema validation needed for empty bodies. Changed annotations from
`skip reason="empty request body"` to proper schema declarations.
* feat: validate catalog examples, add def= and ellipsis suppression
Add schema $defs targeting via def= annotation parameter for schemas
that define request/response types in $defs (catalog_search,
catalog_lookup). The harness resolves the schema, extracts the named
$def, and validates against it.
Fix ellipsis handling: [ ... ] and { ... } now preserve field presence
(as [] and {}) instead of stripping the key entirely. Validation errors
at ellipsis-acknowledged paths are suppressed — the coverage check
already verified the field was acknowledged.
Catalog bugs caught and fixed:
- catalog/rest.md: variant missing required description field
- catalog/index.md: product missing description and price_range
- catalog/rest.md: error response wrongly annotated as get_product
14 new validated blocks (catalog REST + index).
* chore: improve overview.md skip annotations with precise reasons
Replace blanket "conceptual/profile fragment" with specific reasons:
service endpoint fragment, schema definition, profile structure,
JSON-RPC transport/error format, payment handler advertisement, etc.
Validate the one block that maps cleanly to a schema (version
unsupported error response → error_response).
1 validated, 30 skipped with descriptive reasons.
* chore: overview.md — validate 2 blocks, precise skip reasons
Validate: complete checkout request (line 1282), version error response
(line 1910). The rest have specific skip reasons:
- 8x JSON-RPC transport (needs payload_path= feature)
- 8x profile documents (ucp metadata nested under "ucp" key, no
wrapper schema — needs payload_path=$.ucp to extract inner object)
- 3x schema definitions (meta-schemas, not data payloads)
- 2x invalid JSON (contains // comments)
- 6x fragments and declarations
* chore: reconcile annotations after main rebase
Main has moved 28 commits since branch point. Reconcile in two parts:
1. Annotate 11 new doc blocks introduced upstream:
- core-concepts.md (2): capability declaration + profile fragment
- schema-authoring.md (3): cancellation_reason / status / totals
anti-pattern from the new "extensibility" section
- identity-linking.md (5): scope metadata, OAuth token response,
profile documents, incomplete checkout fragment (#354 OAuth rewrite)
- overview.md (1): attribution fragment (#391)
All map cleanly to existing skip-reason taxonomy.
2. Fix checkout-rest.md:1298 — upstream rewrote the example in #371
but dropped required top-level fields. Added "currency" and "links"
to satisfy the schema. The validator caught the bug; this is exactly
the drift-prevention this work exists for.
After this commit:
69 passed, 1 failed, 0 errors, 209 skipped
The single remaining failure (discount.md:336 trailing comma) is fixed
by the next commit (// comment + trailing comma stripping).
* feat: strip // comments and trailing commas, validate 8 more blocks
Documentation examples occasionally use JSONC conventions — line
comments to annotate fields, trailing commas for diff hygiene. The
content is still meaningful UCP payload; only the syntax is non-strict.
Add a preprocessing step that strips // comments (outside strings) and
trailing commas before json.loads. Pattern-matching, not a full JSONC
parser — the doc corpus is well-behaved enough that the simpler approach
holds. If we ever hit a false positive (e.g. a string containing `//`
or `,]`), swap in a real JSONC tokenizer.
Convert 8 previously-skipped blocks to validated:
- ap2-mandates.md (2): merchant_authorization checkout response,
AP2 complete payment request
- buyer-consent.md (1): consent-bound checkout response
- checkout-rest.md (4): three update requests + one read response
- overview.md (2): two complete checkout requests with // annotations
After this commit:
79 passed, 0 failed, 0 errors, 200 skipped
* fix(docs): remove trailing commas in JSON examples
Four examples used trailing commas before closing braces, which is
invalid RFC 8259 JSON. The validator's previous lenient stripper
masked these — they parse correctly with strict JSON.
- ap2-mandates.md: card display.description
- checkout-rest.md: line_items[0].quantity in update example
- discount.md: item.id in cart create (introduced via #371)
- overview.md: payment.instruments token, ap2.checkout_mandate
These fixes stand on their own as bug fixes against strict JSON.
A follow-up commit removes the validator's trailing-comma tolerance.
* feat(validator): formalize JSON example contract
Document the bespoke JSON capability set used in spec examples and
align the validator with that contract. The author-facing guide and
the implementation-facing module docstring describe the same
three-layer model and MUST stay in sync.
Author guide (docs/documentation/schema-authoring.md):
New "Documenting JSON Examples" section covering annotation grammar,
supported authoring conveniences (line comments, template variable,
HTTP envelope, elision markers), explicitly-unsupported features,
skip-reason taxonomy, and common patterns.
Validator (scripts/validate_examples.py):
- Replace module docstring with the full three-layer contract.
- Restructure pipeline into named layer functions:
reduce_to_canonical_json (Layer 1 -> 2)
parse_example (Layer 2 boundary)
process_block (Layer 3 orchestrator)
- Drop trailing-comma stripping. Was tolerated; now rejected to keep
wire-format honesty. The four corpus violations were fixed in the
previous commit.
- Reject unknown annotation attribute keys (catches typos like
"shema=" before they become confusing errors).
- Reject multiple stacked annotations before a fence.
- Track non-json fence state -- annotations inside any fenced block
are documentation, not directives, and are now ignored.
Run: python3 scripts/test_validate_examples.py
* feat(validator): validate profile and docs examples
Make the docs validator explicit about the small amount of authoring syntax it
accepts and route final validation back through `ucp-schema`. `extract=` selects
the nested UCP payload from a displayed envelope, `target=` splices fragments
into a scaffold and selects the matching sub-schema, and `def=` selects profile
or schema variants without requiring reader-facing docs to expose JSON Pointer
machinery. Full examples no longer need fake empty scaffolds; fragments still
require a real parent scaffold so the final object under validation is
meaningful.
Treat profile documents as first-class protocol artifacts rather than a
discovery implementation detail. Move the canonical profile schema to
`source/schemas/profile.json`, remove the duplicate discovery schema, and inline
the reader-facing profile contract into Overview where profile negotiation is
already explained. The schema keeps a variant-neutral wrapper with
business/platform validation definitions, requires the registries needed for
negotiation, keeps `capabilities` optional, and constrains `signing_keys` to
public EC JWKs so the profile surface stays aligned with the signature spec.
Convert profile, capability, MCP/JSON-RPC, and fragment examples to validated
annotations and repair examples where stricter validation exposed drift. MCP
text fallbacks now use abbreviated serialized JSON instead of unhelpful prose,
and remaining skipped blocks are limited to non-UCP payloads, schema-authoring
examples, or formats that do not get a validation schema in this change.
Add dependency-free validator contract tests for the annotation grammar,
extraction, target insertion, ellipsis lowering, template expansion, HTTP body
unwrapping, implicit full-example scaffolds, and trailing-comma rejection.
Validated with `python3 scripts/test_validate_examples.py`,
`python3 scripts/validate_examples.py --schema-base source/schemas/ --audit`,
`python3 scripts/validate_examples.py --schema-base source/schemas/`,
`ucp-schema lint source/`, and pre-commit on the changed files.
* feat(validator): transport envelope schemas
Add four narrow envelope schemas under source/schemas/transports/ so
documentation examples that previously had to skip with reason="JSON-RPC
transport binding" / "embedded protocol binding" / "A2A transport binding"
can be machine-validated:
- jsonrpc.json: JSON-RPC 2.0 request / success response / error response
- mcp_tool_call.json: UCP's MCP tools/call mapping (params.name,
params.arguments, result.structuredContent)
- embedded_message.json: Embedded Protocol envelope and ec.* / ep.cart.*
method namespaces
- a2a_message.json: UCP's A2A mapping points (Agent Card extension
advertisement, Message request wrappers, JSON-RPC responses carrying
A2A Message results)
Each schema validates only UCP's mapping into the underlying transport,
not the transport protocol itself. Operation payloads inside these
envelopes remain validated by the relevant capability schema.
* docs: validate transport binding examples
Convert ~30 previously skipped transport-binding blocks to validated
annotations against the new transports/* schemas:
- cart-mcp, order-mcp JSON-RPC tools/call requests now validate against
transports/mcp_tool_call def=request
- embedded-cart, embedded-checkout, embedded-protocol messages validate
against transports/embedded_message (def=request / response /
error_response)
- overview.md JSON-RPC error responses validate against
transports/jsonrpc def=error_response
- signatures.md JWK validates against profile.signing_keys via target=;
JSON-RPC error response against transports/jsonrpc
Drive-by cleanups in the same files:
- cart-rest.md, order-rest.md: HTTP-headers-only blocks moved from
```json (with skip) to ```http fences
- cart-rest.md: cancel block converted from skip to real op=cancel
validation
- embedded-checkout.md: JWT structure example moved to ```text fence
(not JSON)
- embedded-cart, embedded-checkout, embedded-protocol: replace
`{/* ... */}` JS-comment elision pseudo-syntax with the validator's
canonical bare-form `{ ... }` / `[ ... ]`
* a2a: wrap examples in JSON-RPC envelopes
A2A `message/send` is JSON-RPC 2.0. Prior examples on this page showed
bare A2A Message payloads, which was internally consistent but didn't
match what an implementer sees on the wire — and couldn't be validated
end-to-end against the A2A binding's envelope shape.
Rewrap every example as a full JSON-RPC request/response:
jsonrpc: "2.0"
id: <synthetic 1..N>
method: "message/send"
params: { message: { ... } }
Also replace JS-spread pseudo-syntax (`{...checkoutObject}`,
`...paymentObject`) with the validator's canonical bare-form elision
markers (`{ ... }`), and flip skip annotations to validate against
transports/a2a_message (def=message_request / def=message_response /
def=agent_card).
No normative change to the A2A binding contract — just bringing the
shown payloads in line with what implementations actually exchange.
* point error_response annotations at common/types
Upstream main moved 15 widely-shared types from source/schemas/shopping/types/
to source/schemas/common/types/ (#436) to enable cross-vertical reuse — most
recently the loyalty extension's common/loyalty.json depends on the relocated
amount.json.
Update every `schema=shopping/types/error_response` annotation on our branch
to `schema=common/types/error_response` so the validator can resolve the
schema at its new location. 31 annotations across 14 spec docs; pure path
swap, no semantic change.
* validate loyalty extension examples
The loyalty extension (#340) landed 12 ```json blocks in loyalty.md with
no annotations, which trips the validator's "no unannotated blocks" rule.
Annotate each block against the right schema; no changes to the example
content itself.
* enforce example validation in pre-commit + CI
CI (.github/workflows/docs.yml):
- Add `scripts/**` to the pull_request paths filter so PRs touching the
validator itself trigger the workflow.
- After the existing `ucp-schema lint source/` step, run the full-corpus
example validator and its unit tests. Both are fast (~13s combined)
and use tooling already in setup-build-env (Python via uv, ucp-schema
binary via cargo install). CI is the mandatory backstop.
Validator API (scripts/validate_examples.py):
- `--file` now accepts one or more paths (nargs='+') instead of a single
path. Backward compatible — single-file usage still works — but enables
incremental validation when a tool (or pre-commit) knows which files
actually changed.
pre-commit (.pre-commit-config.yaml):
Three hooks at the pre-commit and pre-push stages, scoped by file filter
for fast local feedback:
- validate-examples-changed: fires on docs/*.md edits. Validates only
the changed files via `--file <paths>` (~0.6s per file, typically
<2s total). Catches direct annotation/schema-binding errors at commit
time.
- validate-examples-full: fires on source/schemas/* or validator-code
edits. Runs the full corpus (~12s) because a single schema change
can invalidate examples across many docs — the cross-file regression
case the incremental hook intentionally trades off.
- validate-examples-tests: fires on validator code/test edits. Runs the
43 unit tests (~0.3s).
All three use `language: system`, requiring Python (already needed for
ruff/pre-commit) and the ucp-schema binary on PATH (cargo install
ucp-schema). Contributors opt in by running:
pre-commit install --hook-type pre-commit --hook-type pre-push
Without the install step, contributors get caught at PR time by CI.
With it, they get early feedback locally. CI remains the mandatory
gate; pre-commit/pre-push is the opt-in early-warning system.
* document validator setup, hooks, and CI enforcement
The "Running the validator locally" subsection of the schema-authoring
guide was a three-line code snippet. Now that the validator is enforced
via pre-commit hooks and CI, authors need to know:
- How to install the tooling (uv sync + cargo install ucp-schema +
pre-commit install --hook-type pre-commit --hook-type pre-push).
- Why both hook types matter: pre-commit only installs the pre-commit
stage hook by default, but this repo also uses pre-push hooks as a
safety net against --no-verify bypasses.
- What runs automatically and where — a table covering the three
enforcement surfaces (pre-commit changed-files, pre-commit/pre-push
full-corpus, CI full-corpus) with scope and trigger.
- Why the full-corpus check fires on schema/validator edits but not on
pure doc edits — the cross-file regression case the incremental
hook intentionally trades off.
* feat: schema-validated documentation examples
Introduce a validation harness that ensures every JSON example in the spec
docs is either validated against UCP schemas or explicitly skipped. This
catches documentation drift — when schemas change, stale examples break CI
instead of silently misleading readers.
## Contract
Every ```json block requires an annotation on the preceding line:
<!-- ucp:example schema=shopping/checkout op=read -->
<!-- ucp:example schema=shopping/checkout path=$.totals op=read -->
<!-- ucp:example schema=shopping/cart op=create direction=request -->
<!-- ucp:example skip reason="JSON-RPC transport binding" -->
Unannotated blocks are hard failures.
Principle: "if you open it, you own it." When an example opens an object's
braces, every required field (per the resolved schema for the declared
op + direction) must be shown or acknowledged. This applies recursively.
Three-tier value contract for annotated (non-skip) examples:
- Full value: validated against schema (types, constraints, enums)
- Required fields ({ ... }, [ ... ], "..."): field must exists but
values are optional; scaffold fills it for validation.
- Absent fields only permitted for optional schema fields
## Pipeline
1. Extract — find annotated ```json blocks (handles tab indentation)
2. Unwrap — strip HTTP headers from request/response examples
3. Expand — substitute {{ ucp_version }} with valid date
4. Preprocess — convert bare ... to valid JSON ({ ... } → {"...":"..."})
5. Coverage — walk resolved schema, verify required fields acknowledged
6. Strip — remove ellipsis markers
7. Merge — deep-merge into scaffold (example wins, scaffold fills gaps)
8. Validate — ucp-schema validate on merged payload
9. Report — map errors to source file:line
## Bugs caught and fixed
- checkout.md: item missing required `price` field in disclosure example
- checkout.md: totals array missing required `subtotal` entry
- checkout-rest.md: business outcome used non-schema `available_quantity`
field on line_item; replaced with message-based pattern
- discount.md (4 instances): `quantity` wrongly nested inside `item`
object instead of on `line_item` — schema requires quantity at line_item
level
- discount.md: `price` and `title` shown in create requests where schema
omits them (request-only fields: only `item.id` is valid)
## Current state
268 blocks across 39 files:
- 52 validated (REST request/response pairs, core spec examples, error
responses, extension examples)
- 216 skipped (transport bindings, schema definitions, configs — future
work: payload_path= for JSON-RPC unwrap)
Run: python3 scripts/validate_examples.py --schema-base source/schemas/
* fix: validate empty request bodies instead of skipping
Address review feedback (drewolson-google): empty `{}` request bodies
for GET and cancel operations are valid requests, not skip-worthy.
The harness now recognizes `{}` as trivially valid — no coverage or
schema validation needed for empty bodies. Changed annotations from
`skip reason="empty request body"` to proper schema declarations.
* feat: validate catalog examples, add def= and ellipsis suppression
Add schema $defs targeting via def= annotation parameter for schemas
that define request/response types in $defs (catalog_search,
catalog_lookup). The harness resolves the schema, extracts the named
$def, and validates against it.
Fix ellipsis handling: [ ... ] and { ... } now preserve field presence
(as [] and {}) instead of stripping the key entirely. Validation errors
at ellipsis-acknowledged paths are suppressed — the coverage check
already verified the field was acknowledged.
Catalog bugs caught and fixed:
- catalog/rest.md: variant missing required description field
- catalog/index.md: product missing description and price_range
- catalog/rest.md: error response wrongly annotated as get_product
14 new validated blocks (catalog REST + index).
* chore: improve overview.md skip annotations with precise reasons
Replace blanket "conceptual/profile fragment" with specific reasons:
service endpoint fragment, schema definition, profile structure,
JSON-RPC transport/error format, payment handler advertisement, etc.
Validate the one block that maps cleanly to a schema (version
unsupported error response → error_response).
1 validated, 30 skipped with descriptive reasons.
* chore: overview.md — validate 2 blocks, precise skip reasons
Validate: complete checkout request (line 1282), version error response
(line 1910). The rest have specific skip reasons:
- 8x JSON-RPC transport (needs payload_path= feature)
- 8x profile documents (ucp metadata nested under "ucp" key, no
wrapper schema — needs payload_path=$.ucp to extract inner object)
- 3x schema definitions (meta-schemas, not data payloads)
- 2x invalid JSON (contains // comments)
- 6x fragments and declarations
* chore: reconcile annotations after main rebase
Main has moved 28 commits since branch point. Reconcile in two parts:
1. Annotate 11 new doc blocks introduced upstream:
- core-concepts.md (2): capability declaration + profile fragment
- schema-authoring.md (3): cancellation_reason / status / totals
anti-pattern from the new "extensibility" section
- identity-linking.md (5): scope metadata, OAuth token response,
profile documents, incomplete checkout fragment (#354 OAuth rewrite)
- overview.md (1): attribution fragment (#391)
All map cleanly to existing skip-reason taxonomy.
2. Fix checkout-rest.md:1298 — upstream rewrote the example in #371
but dropped required top-level fields. Added "currency" and "links"
to satisfy the schema. The validator caught the bug; this is exactly
the drift-prevention this work exists for.
After this commit:
69 passed, 1 failed, 0 errors, 209 skipped
The single remaining failure (discount.md:336 trailing comma) is fixed
by the next commit (// comment + trailing comma stripping).
* feat: strip // comments and trailing commas, validate 8 more blocks
Documentation examples occasionally use JSONC conventions — line
comments to annotate fields, trailing commas for diff hygiene. The
content is still meaningful UCP payload; only the syntax is non-strict.
Add a preprocessing step that strips // comments (outside strings) and
trailing commas before json.loads. Pattern-matching, not a full JSONC
parser — the doc corpus is well-behaved enough that the simpler approach
holds. If we ever hit a false positive (e.g. a string containing `//`
or `,]`), swap in a real JSONC tokenizer.
Convert 8 previously-skipped blocks to validated:
- ap2-mandates.md (2): merchant_authorization checkout response,
AP2 complete payment request
- buyer-consent.md (1): consent-bound checkout response
- checkout-rest.md (4): three update requests + one read response
- overview.md (2): two complete checkout requests with // annotations
After this commit:
79 passed, 0 failed, 0 errors, 200 skipped
* fix(docs): remove trailing commas in JSON examples
Four examples used trailing commas before closing braces, which is
invalid RFC 8259 JSON. The validator's previous lenient stripper
masked these — they parse correctly with strict JSON.
- ap2-mandates.md: card display.description
- checkout-rest.md: line_items[0].quantity in update example
- discount.md: item.id in cart create (introduced via #371)
- overview.md: payment.instruments token, ap2.checkout_mandate
These fixes stand on their own as bug fixes against strict JSON.
A follow-up commit removes the validator's trailing-comma tolerance.
* feat(validator): formalize JSON example contract
Document the bespoke JSON capability set used in spec examples and
align the validator with that contract. The author-facing guide and
the implementation-facing module docstring describe the same
three-layer model and MUST stay in sync.
Author guide (docs/documentation/schema-authoring.md):
New "Documenting JSON Examples" section covering annotation grammar,
supported authoring conveniences (line comments, template variable,
HTTP envelope, elision markers), explicitly-unsupported features,
skip-reason taxonomy, and common patterns.
Validator (scripts/validate_examples.py):
- Replace module docstring with the full three-layer contract.
- Restructure pipeline into named layer functions:
reduce_to_canonical_json (Layer 1 -> 2)
parse_example (Layer 2 boundary)
process_block (Layer 3 orchestrator)
- Drop trailing-comma stripping. Was tolerated; now rejected to keep
wire-format honesty. The four corpus violations were fixed in the
previous commit.
- Reject unknown annotation attribute keys (catches typos like
"shema=" before they become confusing errors).
- Reject multiple stacked annotations before a fence.
- Track non-json fence state -- annotations inside any fenced block
are documentation, not directives, and are now ignored.
Run: python3 scripts/test_validate_examples.py
* feat(validator): validate profile and docs examples
Make the docs validator explicit about the small amount of authoring syntax it
accepts and route final validation back through `ucp-schema`. `extract=` selects
the nested UCP payload from a displayed envelope, `target=` splices fragments
into a scaffold and selects the matching sub-schema, and `def=` selects profile
or schema variants without requiring reader-facing docs to expose JSON Pointer
machinery. Full examples no longer need fake empty scaffolds; fragments still
require a real parent scaffold so the final object under validation is
meaningful.
Treat profile documents as first-class protocol artifacts rather than a
discovery implementation detail. Move the canonical profile schema to
`source/schemas/profile.json`, remove the duplicate discovery schema, and inline
the reader-facing profile contract into Overview where profile negotiation is
already explained. The schema keeps a variant-neutral wrapper with
business/platform validation definitions, requires the registries needed for
negotiation, keeps `capabilities` optional, and constrains `signing_keys` to
public EC JWKs so the profile surface stays aligned with the signature spec.
Convert profile, capability, MCP/JSON-RPC, and fragment examples to validated
annotations and repair examples where stricter validation exposed drift. MCP
text fallbacks now use abbreviated serialized JSON instead of unhelpful prose,
and remaining skipped blocks are limited to non-UCP payloads, schema-authoring
examples, or formats that do not get a validation schema in this change.
Add dependency-free validator contract tests for the annotation grammar,
extraction, target insertion, ellipsis lowering, template expansion, HTTP body
unwrapping, implicit full-example scaffolds, and trailing-comma rejection.
Validated with `python3 scripts/test_validate_examples.py`,
`python3 scripts/validate_examples.py --schema-base source/schemas/ --audit`,
`python3 scripts/validate_examples.py --schema-base source/schemas/`,
`ucp-schema lint source/`, and pre-commit on the changed files.
* feat(validator): transport envelope schemas
Add four narrow envelope schemas under source/schemas/transports/ so
documentation examples that previously had to skip with reason="JSON-RPC
transport binding" / "embedded protocol binding" / "A2A transport binding"
can be machine-validated:
- jsonrpc.json: JSON-RPC 2.0 request / success response / error response
- mcp_tool_call.json: UCP's MCP tools/call mapping (params.name,
params.arguments, result.structuredContent)
- embedded_message.json: Embedded Protocol envelope and ec.* / ep.cart.*
method namespaces
- a2a_message.json: UCP's A2A mapping points (Agent Card extension
advertisement, Message request wrappers, JSON-RPC responses carrying
A2A Message results)
Each schema validates only UCP's mapping into the underlying transport,
not the transport protocol itself. Operation payloads inside these
envelopes remain validated by the relevant capability schema.
* docs: validate transport binding examples
Convert ~30 previously skipped transport-binding blocks to validated
annotations against the new transports/* schemas:
- cart-mcp, order-mcp JSON-RPC tools/call requests now validate against
transports/mcp_tool_call def=request
- embedded-cart, embedded-checkout, embedded-protocol messages validate
against transports/embedded_message (def=request / response /
error_response)
- overview.md JSON-RPC error responses validate against
transports/jsonrpc def=error_response
- signatures.md JWK validates against profile.signing_keys via target=;
JSON-RPC error response against transports/jsonrpc
Drive-by cleanups in the same files:
- cart-rest.md, order-rest.md: HTTP-headers-only blocks moved from
```json (with skip) to ```http fences
- cart-rest.md: cancel block converted from skip to real op=cancel
validation
- embedded-checkout.md: JWT structure example moved to ```text fence
(not JSON)
- embedded-cart, embedded-checkout, embedded-protocol: replace
`{/* ... */}` JS-comment elision pseudo-syntax with the validator's
canonical bare-form `{ ... }` / `[ ... ]`
* a2a: wrap examples in JSON-RPC envelopes
A2A `message/send` is JSON-RPC 2.0. Prior examples on this page showed
bare A2A Message payloads, which was internally consistent but didn't
match what an implementer sees on the wire — and couldn't be validated
end-to-end against the A2A binding's envelope shape.
Rewrap every example as a full JSON-RPC request/response:
jsonrpc: "2.0"
id: <synthetic 1..N>
method: "message/send"
params: { message: { ... } }
Also replace JS-spread pseudo-syntax (`{...checkoutObject}`,
`...paymentObject`) with the validator's canonical bare-form elision
markers (`{ ... }`), and flip skip annotations to validate against
transports/a2a_message (def=message_request / def=message_response /
def=agent_card).
No normative change to the A2A binding contract — just bringing the
shown payloads in line with what implementations actually exchange.
* point error_response annotations at common/types
Upstream main moved 15 widely-shared types from source/schemas/shopping/types/
to source/schemas/common/types/ (#436) to enable cross-vertical reuse — most
recently the loyalty extension's common/loyalty.json depends on the relocated
amount.json.
Update every `schema=shopping/types/error_response` annotation on our branch
to `schema=common/types/error_response` so the validator can resolve the
schema at its new location. 31 annotations across 14 spec docs; pure path
swap, no semantic change.
* validate loyalty extension examples
The loyalty extension (#340) landed 12 ```json blocks in loyalty.md with
no annotations, which trips the validator's "no unannotated blocks" rule.
Annotate each block against the right schema; no changes to the example
content itself.
* enforce example validation in pre-commit + CI
CI (.github/workflows/docs.yml):
- Add `scripts/**` to the pull_request paths filter so PRs touching the
validator itself trigger the workflow.
- After the existing `ucp-schema lint source/` step, run the full-corpus
example validator and its unit tests. Both are fast (~13s combined)
and use tooling already in setup-build-env (Python via uv, ucp-schema
binary via cargo install). CI is the mandatory backstop.
Validator API (scripts/validate_examples.py):
- `--file` now accepts one or more paths (nargs='+') instead of a single
path. Backward compatible — single-file usage still works — but enables
incremental validation when a tool (or pre-commit) knows which files
actually changed.
pre-commit (.pre-commit-config.yaml):
Three hooks at the pre-commit and pre-push stages, scoped by file filter
for fast local feedback:
- validate-examples-changed: fires on docs/*.md edits. Validates only
the changed files via `--file <paths>` (~0.6s per file, typically
<2s total). Catches direct annotation/schema-binding errors at commit
time.
- validate-examples-full: fires on source/schemas/* or validator-code
edits. Runs the full corpus (~12s) because a single schema change
can invalidate examples across many docs — the cross-file regression
case the incremental hook intentionally trades off.
- validate-examples-tests: fires on validator code/test edits. Runs the
43 unit tests (~0.3s).
All three use `language: system`, requiring Python (already needed for
ruff/pre-commit) and the ucp-schema binary on PATH (cargo install
ucp-schema). Contributors opt in by running:
pre-commit install --hook-type pre-commit --hook-type pre-push
Without the install step, contributors get caught at PR time by CI.
With it, they get early feedback locally. CI remains the mandatory
gate; pre-commit/pre-push is the opt-in early-warning system.
* document validator setup, hooks, and CI enforcement
The "Running the validator locally" subsection of the schema-authoring
guide was a three-line code snippet. Now that the validator is enforced
via pre-commit hooks and CI, authors need to know:
- How to install the tooling (uv sync + cargo install ucp-schema +
pre-commit install --hook-type pre-commit --hook-type pre-push).
- Why both hook types matter: pre-commit only installs the pre-commit
stage hook by default, but this repo also uses pre-push hooks as a
safety net against --no-verify bypasses.
- What runs automatically and where — a table covering the three
enforcement surfaces (pre-commit changed-files, pre-commit/pre-push
full-corpus, CI full-corpus) with scope and trigger.
- Why the full-corpus check fires on schema/validator edits but not on
pure doc edits — the cross-file regression case the incremental
hook intentionally trades off.
refactor: move generic types from
shopping/tocommon/types/Rationale
All types currently live under
shopping/types/, but many of them carry no shopping-specific semantics. As UCP is envisioned to expand to other verticals beyond shopping, those verticals will need the same primitives — money amounts, addresses, pagination, messaging, rich text — without taking a dependency on the shopping namespace. This refactor establishescommon/types/as the canonical home for types that belong to the protocol, not to a vertical.This also fixes a pre-existing namespace violation:
common/identity_linking.jsonwas referencing../shopping/types/description.json, which is the exact problem this refactor corrects structurally.Phases to do the refactor
Phase 1 — Pure primitives
No vertical semantics. Any future capability will need these.
amount,signed_amount,price,description,media,postal_address,pagination,reverse_domain_name,link,buyerPhase 2 — Protocol messaging infrastructure
UCP's error/warning/info messaging pattern. Every vertical reuses this plumbing verbatim.
message,message_error,message_warning,message_info,error_response,error_code,warning_code,info_codePhase 3 — Cross-vertical candidates (future PR)
Strong candidates with clear cross-vertical semantics, deferred to validate design stability first.
attribution,signals,context,ratingPhase 4 — Transactional primitives (future PR)
Financial breakdown types whose schema may need to evolve per vertical before being promoted.
total,totals,price_range,price_filterPhase 5 - Payment types (future PR)
payment_instrument,payment_credential,payment_identity,card_credential,card_payment_instrument,token_credential,available_payment_instrument— these are cross-vertical but domain-specific. Should they be in their ownpayment/types/namespace?This PR (Phase 1 + Phase 2)
source/schemas/common/types/with 18 new files, each with updated$id(https://ucp.dev/schemas/common/types/...)source/schemas/shopping/types/$refpaths in:shopping/types/*.json(remaining shopping types that reference moved types)shopping/*.json(capability schemas: cart, checkout, order, fulfillment, etc.)source/schemas/ucp.jsonsource/schemas/common/identity_linking.json(fixes pre-existing violation)source/services/shopping/(OpenRPC and OpenAPI service definitions)source/schemas/commontoSCHEMAS_DIRSinmain.pyso the docs macro system resolves moved types correctlyNo schema semantics were changed — this is a pure relocation with path updates.