You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
[validation] Close generated Pydantic vs canonical JSON Schema semantic gaps #1102
Generated Pydantic models do not enforce all semantics in the canonical JSON Schemas. In particular, allOf + if/then/else conditionals and x-adcp-validation invariants can silently disappear from runtime model validation.
Reproduction from reporting
The reporting revision schema requires finality_basis, finality_policy_id, and finalized_at when finality == "official", and forbids that evidence for non-official revisions. The generated ReportingRevision Pydantic model accepted an official revision without those fields.
The same implementation also had to enforce canonical media-buy set ordering manually because that behavioral invariant is not represented by the generated model.
Impact
An adopter can construct or accept a Python object that appears validated but is rejected by the canonical protocol schema. For billing/reporting this can turn an unsupported finality assertion into apparently valid application state.
Decision needed
Choose and document one supported boundary:
generated Pydantic models have validation parity with canonical JSON Schema, including conditionals and selected x-adcp-validation rules; or
Pydantic models are structural application types only, and every wire boundary runs a provided canonical-schema validator in addition.
Expected work
Add differential fixtures that validate the same payload with Pydantic and the bundled canonical JSON Schema.
Classification: Bug — validation gap Bucket(s): validation (label not in repo — flagging gap in run summary) Status: ready-for-human
What the experts said:
ad-tech-protocol-expert: Architectural — gap is a real billing-compliance risk; ReportingRevision with missing finality_basis/finality_policy_id breaks the adcp#6953 audit chain; Option 2 (wire-boundary jsonschema validator, already partially implemented in src/adcp/validation/schema_validator.py) is the correct primary path; Option 1 (full Pydantic parity via codegen) is unsound with datamodel-code-generator and breaking.
code-reviewer: Analysis was still completing at post time; protocol expert covered implementation-layer findings from direct codebase inspection.
My take: The core decision — full Pydantic conditional parity vs structural types + wire-boundary validator — is architectural with breaking-change implications that needs a maintainer call before implementation begins. A non-breaking third option exists: narrow @model_validator(mode='after') stubs in _ergonomic.py for billing-critical conditionals only (precedented by src/adcp/validation/legacy.py), but all paths hinge on whether bundled schemas already include the if/then/else blocks for ReportingRevision.
Do bundled schemas (via scripts/sync_schemas.py) already carry the if/then/else conditionals for ReportingRevision? If yes, scope narrows to _ergonomic.py stubs and CI fixtures only — wire-boundary may already be protected.
Does draft PR feat(reporting): add reliable ledger reconciliation #1095 introduce these schema fields, or consume them from an upstream schema that already carries them? (Determines whether a schema re-sync must precede SDK work.)
Does the idempotency replay cache short-circuit validate_response? If so, replay paths are unprotected even with Option 2 in place.
Additional concern: the RequestContext.metadata context-echo contract (CLAUDE.md) means a structurally-incomplete ReportingRevision can replay null-filled fields into governance agents, and discriminated union shadowing may silently misroute an official revision as provisional when co-required fields are absent.
The gap this issue describes is still measurable on 8.0.0, and the differential
fixtures that closed it cover one tool.
tests/fixtures/validation-parity-cases.json holds 8 cases, all on get_signals, request direction only, with 3 allowlisted divergences — all
three conditionals (else/anyOf/required, if/then/not, dependencies). The
expected work in the description reads wider than that:
Measuring the rest: for each tool that ships a request schema, instantiate that
schema into a payload the bundled validator declares valid, replace one leaf at
a time with a value of a different JSON type, and compare the bundled
validator's verdict with the generated model's. Of the 84 tools, 78 yield a
valid payload and 72 of those also expose a request model, which is the set
graded — get_signals is one of the six the instantiator cannot satisfy, so
these fixtures and that measurement cover disjoint tools.
documents compared : 39546
divergences : 5995
the bundled schema accepts a document the model refuses : 4604
the model accepts a document the bundled schema refuses : 1391
17 of the 72 request models that exist refuse a payload the bundled schema
declares valid. Two causes account for most of it, and neither is a
conditional, so neither is covered by the three allowlisted entries:
The bundled validator does not enforce format: uri. schema_loader builds
each validator with a bare FormatChecker() plus a hand-registered date-time, and a bare FormatChecker() carries only the checks whose
optional dependencies import — uri needs rfc3987, which this package does
not depend on. format: uri is the most-used format in the 3.2 bundle and
not one occurrence is checked. This is a defect in the Option 2 path this
issue's triage chose as primary: the wire-boundary validator is weaker than
the schema it loads, and weaker again or stronger depending on what else the
adopter has installed.
Pydantic's lax mode coerces where JSON Schema does not. ListAccountsRequest.model_validate({"sandbox": "yes"}).sandbox is True
while validate_request("list_accounts", {"sandbox": "yes"}).valid is False. Measured against pydantic 2.13, bool accepts 1, 0, "1", "0", "yes", "true"; int accepts "1", True, 1.0; float accepts "1" and True. str coerces nothing, so only those three types diverge.
Both are fixable without the full-codegen-parity path the triage ruled out: one
format checker in schema_loader, and --strict-types bool int float on the
generator (plus an integer annotation that admits an integral float, which JSON
Schema counts as an integer and StrictInt does not).
The remaining divergences — explicit null on an optional field, a date-time
field parsing a number as a Unix timestamp, a format: uri field dropping its
sibling pattern, the canonical model refusing a legacy creative identity the
schema retains — each need a decision rather than a patch, and are enumerated
with counts in the issue linked below.
What stops this reopening on the next version bump is driving the suite off the
bundle instead of a fixture list: enumerate the schemas, synthesize the payloads,
and name the differences by cause rather than by tool and pointer. The three
entries get_signals allowlists today are all one cause — a conditional
subschema that code generation cannot express — and stated that way the entry
covers every tool at once.
Details, measurements and a suite built that way: #1359
Activity
bokelley commentedon Aug 29, 2026
Triage
Classification: Bug — validation gap
Bucket(s): validation (label not in repo — flagging gap in run summary)
Status: ready-for-human
What the experts said:
ReportingRevisionwith missingfinality_basis/finality_policy_idbreaks the adcp#6953 audit chain; Option 2 (wire-boundaryjsonschemavalidator, already partially implemented insrc/adcp/validation/schema_validator.py) is the correct primary path; Option 1 (full Pydantic parity via codegen) is unsound withdatamodel-code-generatorand breaking.My take: The core decision — full Pydantic conditional parity vs structural types + wire-boundary validator — is architectural with breaking-change implications that needs a maintainer call before implementation begins. A non-breaking third option exists: narrow
@model_validator(mode='after')stubs in_ergonomic.pyfor billing-critical conditionals only (precedented bysrc/adcp/validation/legacy.py), but all paths hinge on whether bundled schemas already include theif/then/elseblocks forReportingRevision.Open questions for @bokelley:
scripts/sync_schemas.py) already carry theif/then/elseconditionals forReportingRevision? If yes, scope narrows to_ergonomic.pystubs and CI fixtures only — wire-boundary may already be protected.validate_response? If so, replay paths are unprotected even with Option 2 in place.Additional concern: the
RequestContext.metadatacontext-echo contract (CLAUDE.md) means a structurally-incompleteReportingRevisioncan replay null-filled fields into governance agents, and discriminated union shadowing may silently misroute anofficialrevision asprovisionalwhen co-required fields are absent.Triaged by Claude Code. Session: https://claude.ai/code/session_01FUxGWD4ut2q3aq6UGxt9mm
Generated by Claude Code
fix(validation): enforce canonical schema boundaries (#1117)
KonstantinMirin commentedon Oct 3, 2026
The gap this issue describes is still measurable on 8.0.0, and the differential
fixtures that closed it cover one tool.
tests/fixtures/validation-parity-cases.jsonholds 8 cases, all onget_signals, request direction only, with 3 allowlisted divergences — allthree conditionals (
else/anyOf/required,if/then/not,dependencies). Theexpected work in the description reads wider than that:
Measuring the rest: for each tool that ships a request schema, instantiate that
schema into a payload the bundled validator declares valid, replace one leaf at
a time with a value of a different JSON type, and compare the bundled
validator's verdict with the generated model's. Of the 84 tools, 78 yield a
valid payload and 72 of those also expose a request model, which is the set
graded —
get_signalsis one of the six the instantiator cannot satisfy, sothese fixtures and that measurement cover disjoint tools.
17 of the 72 request models that exist refuse a payload the bundled schema
declares valid. Two causes account for most of it, and neither is a
conditional, so neither is covered by the three allowlisted entries:
The bundled validator does not enforce
format: uri.schema_loaderbuildseach validator with a bare
FormatChecker()plus a hand-registereddate-time, and a bareFormatChecker()carries only the checks whoseoptional dependencies import —
urineedsrfc3987, which this package doesnot depend on.
format: uriis the most-used format in the 3.2 bundle andnot one occurrence is checked. This is a defect in the Option 2 path this
issue's triage chose as primary: the wire-boundary validator is weaker than
the schema it loads, and weaker again or stronger depending on what else the
adopter has installed.
Pydantic's lax mode coerces where JSON Schema does not.
ListAccountsRequest.model_validate({"sandbox": "yes"}).sandboxisTruewhile
validate_request("list_accounts", {"sandbox": "yes"}).validisFalse. Measured against pydantic 2.13,boolaccepts1,0,"1","0","yes","true";intaccepts"1",True,1.0;floataccepts"1"andTrue.strcoerces nothing, so only those three types diverge.Both are fixable without the full-codegen-parity path the triage ruled out: one
format checker in
schema_loader, and--strict-types bool int floaton thegenerator (plus an integer annotation that admits an integral float, which JSON
Schema counts as an integer and
StrictIntdoes not).The remaining divergences — explicit
nullon an optional field, adate-timefield parsing a number as a Unix timestamp, a
format: urifield dropping itssibling
pattern, the canonical model refusing a legacy creative identity theschema retains — each need a decision rather than a patch, and are enumerated
with counts in the issue linked below.
What stops this reopening on the next version bump is driving the suite off the
bundle instead of a fixture list: enumerate the schemas, synthesize the payloads,
and name the differences by cause rather than by tool and pointer. The three
entries
get_signalsallowlists today are all one cause — a conditionalsubschema that code generation cannot express — and stated that way the entry
covers every tool at once.
Details, measurements and a suite built that way: #1359