Skip to content

fix(python): propagate JSON Schema default values to generated class#3050

Open
schani wants to merge 12 commits into
masterfrom
agent/fix-issue-1415
Open

fix(python): propagate JSON Schema default values to generated class#3050
schani wants to merge 12 commits into
masterfrom
agent/fix-issue-1415

Conversation

@schani

@schani schani commented Jul 20, 2026

Copy link
Copy Markdown
Member

Bug

JSON Schema default values on object properties were silently dropped by
quicktype: 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"
}
node dist/index.js --lang python --src-lang schema meta.schema

produced name: str with no default, and from_dict did
from_str(obj.get("Name")) with no fallback — so a document missing "Name"
would raise an AssertionError instead of falling back to "Hello".

Root cause

The default keyword was never read out of the JSON Schema input at all —
nothing in quicktype-core's attribute-producer pipeline stored it on the
type graph, so no renderer could ever see it.

Fix

  • Added packages/quicktype-core/src/attributes/DefaultValues.ts: a new
    TypeAttributeKind (propertyDefaultValues) plus a
    defaultValuesAttributeProducer that reads each object property's
    default (when present) off the JSON Schema and attaches it to the
    object type.
  • Wired the new producer into JSONSchemaInput's attribute-producer list.
  • In the Python renderer (PythonRenderer.ts, JSONPythonRenderer.ts):
    • Emit the default as a Python literal on the dataclass/plain-class field
      (name: str = "Hello"), including support for None, numbers, bools,
      lists, and dicts (mutable defaults use dataclasses.field(default_factory=...)
      where appropriate).
    • Use the default as the fallback argument to obj.get(...) in
      from_dict so round-tripping a document that omits the key still
      produces the schema default.
    • Adjusted sortClassProperties (and the shared ConvenienceRenderer
      base method signature it overrides) so properties with defaults are
      sorted after non-defaulted ones, keeping the generated class valid.
  • Scoped to the Python target, per the issue; other languages are
    unaffected (only Python overrides the touched ConvenienceRenderer
    method).

Test coverage

Added test/inputs/schema/default-values.schema with two defaulted string
properties, a .1.json sample that omits one of them, and a
.1.out.defaults.json expected-output file (matching the repo's existing
per-feature .out.<feature>.json fixture convention) that has the default
filled in. Registered a new "defaults" language feature and enabled it for
Python in test/languages.ts.

Verification

  • npm run build passes cleanly.
  • Manually confirmed the fix against the issue's exact repro: generated
    Python now has name: str = "Hello" and
    obj.get("Name", "Hello").
  • Confirmed the new fixture fails on the pre-fix code (Expected property Greeting not found, exit 1) and passes after the fix.
  • QUICKTEST=true FIXTURE=schema-python script/test — all 72 schema-python
    fixtures pass, including the new one.
  • QUICKTEST=true FIXTURE=python script/test — all 62 JSON python fixtures
    pass (no regressions from the shared ConvenienceRenderer signature
    change).
  • Other Python renderer modes spot-checked manually (plain classes,
    --pydantic-base-model) and all correctly emit the default.
  • CI will run the full fixture matrix for other languages/configurations.

Fixes #1415

🤖 Generated with Claude Code

schani and others added 12 commits July 20, 2026 19:09
…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>
@github-actions

Copy link
Copy Markdown

Generated-output differences

32 files differ — 1 modified, 31 new, 0 deleted
2211 changed lines — +2209 / −2

Open the generated-output report →

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Default values are not propagated to the generated class

1 participant