fix(python): propagate JSON Schema default values to generated class#3050
Open
schani wants to merge 12 commits into
Open
fix(python): propagate JSON Schema default values to generated class#3050schani wants to merge 12 commits into
schani wants to merge 12 commits into
Conversation
…1415) Co-Authored-By: gpt-5.6-sol via pi <noreply@openai.com>
Co-Authored-By: Claude <noreply@anthropic.com>
Use Object.hasOwn() instead of Object.prototype.hasOwnProperty.call() and apply Biome formatting to the Python renderers. Co-Authored-By: Claude <noreply@anthropic.com>
…t (#undefined) quicktype-core's tsconfig targets lib es6, which does not include the ES2022 Object.hasOwn API, breaking the build on all Node CI matrix jobs. Co-Authored-By: gpt-5.6-sol via pi <noreply@openai.com>
…st (#undefined) agent/fix-issue-1415 branched before PR #1289 removed forEachEnumCase's alphabetizing sort, so its CI merge-preview with current master picks up master's now-correct (schema-declaration-order) enum emission. The cjson-enum-default test added by PR #2357 still asserted the old alphabetized order, so it failed regardless of this PR's own changes. Update the expectation to match the intentional, already-fixed behavior. Co-Authored-By: gpt-5.6-sol via pi <noreply@openai.com>
…ults sample (#undefined)
test/inputs/schema/default-values.1.fail.no-defaults.json supplied
"Name": 42, a JSON number, for a schema field typed string. This is
supposed to make every "no-defaults"-feature language's generated
program exit nonzero, but Jackson (kotlin-jackson) leniently coerces
JSON numbers into String fields, so the fixture unexpectedly
succeeded and printed {"Name":"42"} instead of failing, breaking
schema-kotlin-jackson in CI.
Use an object value instead ("Name": {}), which cannot be coerced
into a string by any JSON binding library (unlike numeric-to-string
scalar coercion). Verified locally that this still correctly fails
for schema-python (defaults-filling only applies to *absent*
properties, so it doesn't mask this present-but-wrong-typed value),
schema-javascript, schema-typescript, and schema-typescript-zod.
Co-Authored-By: gpt-5.6-sol via pi <noreply@openai.com>
…faults fail sample (#3050) test/inputs/schema/default-values.1.fail.no-defaults.json supplied "Name": {}, a JSON object, for a schema field typed string. This is supposed to make every "no-defaults"-feature language's generated program exit nonzero, but Klaxon (the JSON library the plain kotlin/schema-kotlin fixture pins) silently coerces a JSON object into an empty string when binding to a String field, so the fixture unexpectedly succeeded and printed {"Name":""} instead of failing, breaking kotlin/schema-kotlin in CI (and, via fail-fast, cancelling every other language job in the matrix). Use a JSON array instead ("Name": []), which both Klaxon and Jackson reject with a binding exception (verified locally against the pinned klaxon-3.0.1.jar and the kotlin-jackson fixture's jars). Kotlin's generated code does correctly enforce this negative case given the right test value, so the fix is to the fixture data, not to loosen Kotlin's (or kotlin-jackson's/kotlinx's) "no-defaults" feature claim in test/languages.ts, which would silently drop real coverage. Verified locally with FIXTURE=schema-kotlin, FIXTURE=schema-kotlin-jackson, and FIXTURE=schema-kotlinx against test/inputs/schema/default-values.schema: all three now pass. npm run test:unit passes (194/194). Co-Authored-By: gpt-5.6-sol via pi <noreply@openai.com>
…lidate struct field types (#3050) test/inputs/schema/default-values.1.fail.no-defaults.json is meant to make every "no-defaults"-feature language's generated program exit nonzero on a type-mismatched required field. Elixir's generated from_map/1 assigns Jason-decoded values straight into the struct with no runtime type check (the @type spec is a Dialyzer annotation only), so no value we could put in that fail sample will ever make it fail - confirmed by inspecting the generated TopLevel.ex for default-values.schema. This is the same limitation ElixirLanguage.skipSchema already documents and works around for required.schema, intersection.schema (both of which also ship a .fail.no-defaults.json sample), strict-optional.schema, boolean-subschema.schema, and optional-any.schema via the comment "Struct keys cannot be enforced at runtime in Elixir and their values will just be set to null." default-values.schema is a new instance of the same category that was missed when it was added; add it to that same skip group instead of loosening Elixir's "no-defaults" feature claim (which is legitimate for other schemas) or trying to find a magic JSON value. Verified with npm run build, npm run test:unit (194/194), npx biome check test/languages.ts, and by confirming test/fixtures.ts's skip check now matches default-values.schema the same way it already matches required.schema. mix/elixir tooling isn't available in this environment to run the fixture directly. Co-Authored-By: gpt-5.6-sol via pi <noreply@openai.com>
…ull on decode failure (#3050) Same root cause and fix pattern as the Elixir skip added in the previous commit: test/fixtures/haskell/Main.hs decodes to Maybe TopLevel and does `BS.putStr $ encode dec` unconditionally, so a failed Aeson decode (e.g. the "Name": [] type mismatch in default-values.1.fail.no-defaults.json) prints the JSON value `null` and exits 0 instead of failing - no fail sample can ever make this driver exit nonzero. HaskellLanguage.skipSchema already documents and works around this exact limitation ("The test driver encodes the Maybe result, so a failed decode prints "null" and exits 0 — expected-failure samples cannot be detected.") for intersection.schema and required.schema, both of which also ship their own .fail.no-defaults.json sample. default-values.schema is a new instance of the same category that was missed when it was added; add it to the same skip group. Verified with npm run build, npm run test:unit (194/194), npx biome check test/languages.ts, and by confirming default-values.schema now matches HaskellLanguage.skipSchema the same way required.schema already does. stack/GHC tooling isn't available in this environment to run the fixture directly. Also audited the rest of test/languages.ts for other no-defaults-feature languages with this same silent-success pattern; none found missing this skip. Co-Authored-By: gpt-5.6-sol via pi <noreply@openai.com>
Generated-output differences32 files differ — 1 modified, 31 new, 0 deleted |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Bug
JSON Schema
defaultvalues on object properties were silently dropped byquicktype: they never appeared on the generated Python class, and were never
used as a fallback when the corresponding key was missing from the input
dict.
Repro (from #1415):
{ "definitions": { "record:Metadata": { "type": "object", "required": ["Name"], "additionalProperties": false, "properties": { "Name": { "default": "Hello", "type": "string" } } } }, "$ref": "#/definitions/record:Metadata" }produced
name: strwith no default, andfrom_dictdidfrom_str(obj.get("Name"))with no fallback — so a document missing"Name"would raise an
AssertionErrorinstead of falling back to"Hello".Root cause
The
defaultkeyword was never read out of the JSON Schema input at all —nothing in
quicktype-core's attribute-producer pipeline stored it on thetype graph, so no renderer could ever see it.
Fix
packages/quicktype-core/src/attributes/DefaultValues.ts: a newTypeAttributeKind(propertyDefaultValues) plus adefaultValuesAttributeProducerthat reads each object property'sdefault(when present) off the JSON Schema and attaches it to theobject type.
JSONSchemaInput's attribute-producer list.PythonRenderer.ts,JSONPythonRenderer.ts):(
name: str = "Hello"), including support forNone, numbers, bools,lists, and dicts (mutable defaults use
dataclasses.field(default_factory=...)where appropriate).
obj.get(...)infrom_dictso round-tripping a document that omits the key stillproduces the schema default.
sortClassProperties(and the sharedConvenienceRendererbase method signature it overrides) so properties with defaults are
sorted after non-defaulted ones, keeping the generated class valid.
unaffected (only Python overrides the touched
ConvenienceRenderermethod).
Test coverage
Added
test/inputs/schema/default-values.schemawith two defaulted stringproperties, a
.1.jsonsample that omits one of them, and a.1.out.defaults.jsonexpected-output file (matching the repo's existingper-feature
.out.<feature>.jsonfixture convention) that has the defaultfilled in. Registered a new
"defaults"language feature and enabled it forPython in
test/languages.ts.Verification
npm run buildpasses cleanly.Python now has
name: str = "Hello"andobj.get("Name", "Hello").Expected property Greeting not found, exit 1) and passes after the fix.QUICKTEST=true FIXTURE=schema-python script/test— all 72 schema-pythonfixtures pass, including the new one.
QUICKTEST=true FIXTURE=python script/test— all 62 JSON python fixturespass (no regressions from the shared
ConvenienceRenderersignaturechange).
--pydantic-base-model) and all correctly emit the default.Fixes #1415
🤖 Generated with Claude Code