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
from_json_schema() hand-rolls JSON Schema parsing. That parser is incomplete, and we have been completing it one spelling at a time, one bug report at a time. We should delegate the commodity half of the work to a real JSON Schema library and keep only the half that is genuinely ours.
This is an architecture issue, not a bug. Nothing here blocks the fixes currently in flight, and I am explicitly not proposing we hold them.
The evidence
Every one of these is the same underlying problem surfacing in a different spelling:
removed a duplicate type classifier that existed because of this drift
Eight items. Every one is "we did not handle a spelling the spec allows." That is the signature of a hand-rolled parser: coverage grows by bug report rather than by conformance.
Two things make it concrete:
1. We break on Pydantic's own output.anyOf [X, {"type": "null"}] is exactly what Pydantic v2 emits for every Optional field:
So before #198, from_json_schema(SomeModel.model_json_schema()) raised on any model with an optional field. We were failing to ingest the output of the library we are built on.
2. Two in-flight PRs now conflict in this file.#159 and #198 both rewrite convert_property_to_field, and #198 does not apply on top of #159. That is a coordination cost created by having all schema semantics funnel through one hand-written dispatcher.
What is actually ours, and what is not
json_schema_field_converter.py is 660 lines doing two unrelated jobs:
Job 1 — JSON Schema to Python types.$ref resolution, anyOf/oneOf/allOf, nullable spellings, implicit objects, array items, required/default handling. This is a commodity, solved problem, and it is where all eight items above live.
Job 2 — x-aws-stickler-* to comparison semantics. Mapping extensions to comparator, threshold, weight, and clip_under_threshold, then emitting ComparableField() so compare_with() can score:
This is genuinely ours and not replaceable. No off-the-shelf importer produces it; it is the point of the library.
To be clear about one thing, since it is the obvious first question: Pydantic cannot do Job 1 for us. Its JSON Schema support is deliberately one-directional.
So from_json_schema() is not duplicating Pydantic. It is filling a gap Pydantic left open on purpose. The problem is not that the layer exists; it is that we wrote a schema parser inside it.
Proposal
Keep from_json_schema() as the public API. Change its internals to delegate Job 1 and keep Job 2:
The second arrow already largely exists as StructuredModel.from_pydantic(), built for the 0.6.0 zero-config work. That is what makes this tractable rather than a rewrite.
Expected outcome: most of the 660 lines of type dispatch goes away, oneOf/allOf/$ref edge cases come for free, and the class of bug represented by the eight items above stops recurring.
Open questions to answer before committing
This needs a real evaluation, not just a dependency swap:
Behavioral parity. Does anything currently accepted start being rejected, or vice versa? This is a public API with users, so we need a difference report across a corpus of real schemas, not spot checks.
Summary
from_json_schema()hand-rolls JSON Schema parsing. That parser is incomplete, and we have been completing it one spelling at a time, one bug report at a time. We should delegate the commodity half of the work to a real JSON Schema library and keep only the half that is genuinely ours.This is an architecture issue, not a bug. Nothing here blocks the fixes currently in flight, and I am explicitly not proposing we hold them.
The evidence
Every one of these is the same underlying problem surfacing in a different spelling:
from_json_schema()fails on valid nullable schemas (anyOf,type: [X, null], implicit objects)type: ["string", "null"]anyOf [X, null]and implicit objects; still does not handleoneOfOptional[T]annotations soNonedefaults validateX | None, the Python-side mirror of the same gap_unwrap_optionalis PEP-604-blind, so schema export emits{"type": "string"}forX | NoneEight items. Every one is "we did not handle a spelling the spec allows." That is the signature of a hand-rolled parser: coverage grows by bug report rather than by conformance.
Two things make it concrete:
1. We break on Pydantic's own output.
anyOf [X, {"type": "null"}]is exactly what Pydantic v2 emits for everyOptionalfield:So before #198,
from_json_schema(SomeModel.model_json_schema())raised on any model with an optional field. We were failing to ingest the output of the library we are built on.2. Two in-flight PRs now conflict in this file. #159 and #198 both rewrite
convert_property_to_field, and #198 does not apply on top of #159. That is a coordination cost created by having all schema semantics funnel through one hand-written dispatcher.What is actually ours, and what is not
json_schema_field_converter.pyis 660 lines doing two unrelated jobs:Job 1 — JSON Schema to Python types.
$refresolution,anyOf/oneOf/allOf, nullable spellings, implicit objects, array items, required/default handling. This is a commodity, solved problem, and it is where all eight items above live.Job 2 —
x-aws-stickler-*to comparison semantics. Mapping extensions to comparator, threshold, weight, andclip_under_threshold, then emittingComparableField()socompare_with()can score:This is genuinely ours and not replaceable. No off-the-shelf importer produces it; it is the point of the library.
To be clear about one thing, since it is the obvious first question: Pydantic cannot do Job 1 for us. Its JSON Schema support is deliberately one-directional.
So
from_json_schema()is not duplicating Pydantic. It is filling a gap Pydantic left open on purpose. The problem is not that the layer exists; it is that we wrote a schema parser inside it.Proposal
Keep
from_json_schema()as the public API. Change its internals to delegate Job 1 and keep Job 2:The second arrow already largely exists as
StructuredModel.from_pydantic(), built for the 0.6.0 zero-config work. That is what makes this tractable rather than a rewrite.Expected outcome: most of the 660 lines of type dispatch goes away,
oneOf/allOf/$refedge cases come for free, and the class of bug represented by the eight items above stops recurring.Open questions to answer before committing
This needs a real evaluation, not just a dependency swap:
x-aws-stickler-*, Job 2 has nothing to read and the whole plan fails. This is the make-or-break question. Related: [BUG]: unrecognized x-aws-stickler-* keys are silently dropped, and explain() reports the fallback as "explicit" #210, where unrecognized extension keys are already silently dropped.oneOf,allOf,$ref(including remote and recursive),patternProperties, and the nullable spellings we care about?import sticklerdown to 422 modules with six core dependencies. A schema library must not undo that, so it likely belongs behind an extra or must be genuinely light.json-schema-to-pydantic. There are others. Someone should compare at least two against the above.Explicitly out of scope
from_json_schema()orto_json_schema().x-aws-stickler-*.Filed for 0.8.0 rather than 0.7.0 deliberately: doing this while several PRs are open against the same file would be the wrong order.