From 419a112db5470146b97173b5b52d1a67fadc7bc6 Mon Sep 17 00:00:00 2001 From: Mark Probst Date: Mon, 20 Jul 2026 19:09:30 -0400 Subject: [PATCH 1/9] fix(python): propagate JSON Schema default values to generated class (#1415) Co-Authored-By: gpt-5.6-sol via pi --- .../quicktype-core/src/ConvenienceRenderer.ts | 2 + .../src/attributes/DefaultValues.ts | 84 +++++++++++++ .../src/input/JSONSchemaInput.ts | 2 + .../src/language/Python/JSONPythonRenderer.ts | 13 +- .../src/language/Python/PythonRenderer.ts | 117 ++++++++++++++++-- test/inputs/schema/default-values.1.json | 3 + .../schema/default-values.1.out.defaults.json | 4 + test/inputs/schema/default-values.schema | 20 +++ test/languages.ts | 2 + 9 files changed, 237 insertions(+), 10 deletions(-) create mode 100644 packages/quicktype-core/src/attributes/DefaultValues.ts create mode 100644 test/inputs/schema/default-values.1.json create mode 100644 test/inputs/schema/default-values.1.out.defaults.json create mode 100644 test/inputs/schema/default-values.schema diff --git a/packages/quicktype-core/src/ConvenienceRenderer.ts b/packages/quicktype-core/src/ConvenienceRenderer.ts index de5901e6e2..c9922fca23 100644 --- a/packages/quicktype-core/src/ConvenienceRenderer.ts +++ b/packages/quicktype-core/src/ConvenienceRenderer.ts @@ -919,6 +919,7 @@ export abstract class ConvenienceRenderer extends Renderer { } protected sortClassProperties( + _o: ObjectType, properties: ReadonlyMap, propertyNames: ReadonlyMap, ): ReadonlyMap { @@ -947,6 +948,7 @@ export abstract class ConvenienceRenderer extends Renderer { ): void { const propertyNames = defined(this._propertyNamesStoreView).get(o); const sortedProperties = this.sortClassProperties( + o, o.getProperties(), propertyNames, ); diff --git a/packages/quicktype-core/src/attributes/DefaultValues.ts b/packages/quicktype-core/src/attributes/DefaultValues.ts new file mode 100644 index 0000000000..83f0ca1b07 --- /dev/null +++ b/packages/quicktype-core/src/attributes/DefaultValues.ts @@ -0,0 +1,84 @@ +import { mapFromObject } from "collection-utils"; + +import type { + JSONSchemaAttributes, + JSONSchemaType, + Ref, +} from "../input/JSONSchemaInput.js"; +import type { JSONSchema } from "../input/JSONSchemaStore.js"; + +import { TypeAttributeKind, emptyTypeAttributes } from "./TypeAttributes.js"; + +export type PropertyDefaultValues = ReadonlyMap; + +class PropertyDefaultValuesTypeAttributeKind extends TypeAttributeKind { + public constructor() { + super("propertyDefaultValues"); + } + + public get inIdentity(): boolean { + return true; + } + + public requiresUniqueIdentity(_: PropertyDefaultValues): boolean { + return true; + } + + public combine( + attrs: PropertyDefaultValues[], + ): PropertyDefaultValues | undefined { + const result = new Map(); + for (const attr of attrs) { + for (const [name, value] of attr) { + if (!result.has(name)) result.set(name, value); + } + } + + return result.size === 0 ? undefined : result; + } + + public makeInferred(_: PropertyDefaultValues): undefined { + return undefined; + } +} + +export const propertyDefaultValuesTypeAttributeKind: TypeAttributeKind = + new PropertyDefaultValuesTypeAttributeKind(); + +export function defaultValuesAttributeProducer( + schema: JSONSchema, + _ref: Ref, + types: Set, +): JSONSchemaAttributes | undefined { + if ( + typeof schema !== "object" || + !types.has("object") || + typeof schema.properties !== "object" || + schema.properties === null || + Array.isArray(schema.properties) + ) { + return undefined; + } + + const defaults = new Map(); + for (const [name, propertySchema] of mapFromObject(schema.properties)) { + if ( + typeof propertySchema === "object" && + propertySchema !== null && + !Array.isArray(propertySchema) && + Object.prototype.hasOwnProperty.call(propertySchema, "default") + ) { + defaults.set( + name, + (propertySchema as { default: unknown }).default, + ); + } + } + + if (defaults.size === 0) return undefined; + return { + forType: emptyTypeAttributes, + forObject: + propertyDefaultValuesTypeAttributeKind.makeAttributes(defaults), + }; +} diff --git a/packages/quicktype-core/src/input/JSONSchemaInput.ts b/packages/quicktype-core/src/input/JSONSchemaInput.ts index fe9efcbd8e..6ff5411ac0 100644 --- a/packages/quicktype-core/src/input/JSONSchemaInput.ts +++ b/packages/quicktype-core/src/input/JSONSchemaInput.ts @@ -29,6 +29,7 @@ import { minMaxLengthAttributeProducer, patternAttributeProducer, } from "../attributes/Constraints.js"; +import { defaultValuesAttributeProducer } from "../attributes/DefaultValues.js"; import { descriptionAttributeProducer } from "../attributes/Description.js"; import { enumValuesAttributeProducer } from "../attributes/EnumValues.js"; import { StringTypes } from "../attributes/StringTypes.js"; @@ -1559,6 +1560,7 @@ export class JSONSchemaInput implements Input { ) { this._attributeProducers = [ descriptionAttributeProducer, + defaultValuesAttributeProducer, accessorNamesAttributeProducer, enumValuesAttributeProducer, uriSchemaAttributesProducer, diff --git a/packages/quicktype-core/src/language/Python/JSONPythonRenderer.ts b/packages/quicktype-core/src/language/Python/JSONPythonRenderer.ts index 20d5fc5aa8..310a63eb7c 100644 --- a/packages/quicktype-core/src/language/Python/JSONPythonRenderer.ts +++ b/packages/quicktype-core/src/language/Python/JSONPythonRenderer.ts @@ -976,8 +976,19 @@ export class JSONPythonRenderer extends PythonRenderer { const args: Sourcelike[] = []; this.emitLine("assert isinstance(obj, dict)"); this.forEachClassProperty(t, "none", (name, jsonName, cp) => { + const defaultValue = + this.defaultValueForClassProperty(t, jsonName); const property = { - value: ["obj.get(", this.string(jsonName), ")"], + value: + defaultValue === undefined + ? ["obj.get(", this.string(jsonName), ")"] + : [ + "obj.get(", + this.string(jsonName), + ", ", + this.pythonLiteral(defaultValue), + ")", + ], }; this.emitLine( name, diff --git a/packages/quicktype-core/src/language/Python/PythonRenderer.ts b/packages/quicktype-core/src/language/Python/PythonRenderer.ts index e0d60263ab..5bac7414b7 100644 --- a/packages/quicktype-core/src/language/Python/PythonRenderer.ts +++ b/packages/quicktype-core/src/language/Python/PythonRenderer.ts @@ -7,6 +7,7 @@ import { setUnionInto, } from "collection-utils"; +import { propertyDefaultValuesTypeAttributeKind } from "../../attributes/DefaultValues.js"; import { ConvenienceRenderer, type ForbiddenWordsInfo, @@ -16,7 +17,7 @@ import type { RenderContext } from "../../Renderer.js"; import type { OptionValues } from "../../RendererOptions/index.js"; import type { Sourcelike } from "../../Source.js"; import { stringEscape } from "../../support/Strings.js"; -import { defined, panic } from "../../support/Support.js"; +import { defined, isStringMap, panic } from "../../support/Support.js"; import type { TargetLanguage } from "../../TargetLanguage.js"; import { followTargetType } from "../../Transformers.js"; import { @@ -25,6 +26,7 @@ import { ClassType, EnumType, MapType, + type ObjectType, type Type, UnionType, } from "../../Type/index.js"; @@ -118,6 +120,83 @@ export class PythonRenderer extends ConvenienceRenderer { return [openQuote, stringEscape(s), '"']; } + protected pythonLiteral(value: unknown): Sourcelike { + if (value === null) return "None"; + + switch (typeof value) { + case "string": + return this.string(value); + case "number": + return value.toString(); + case "boolean": + return value ? "True" : "False"; + case "object": + if (Array.isArray(value)) { + return [ + "[", + arrayIntercalate( + ", ", + value.map((item) => this.pythonLiteral(item)), + ), + "]", + ]; + } + + if (isStringMap(value)) { + return [ + "{", + arrayIntercalate( + ", ", + Object.entries(value).map( + ([key, item]) => [ + this.string(key), + ": ", + this.pythonLiteral(item), + ], + ), + ), + "}", + ]; + } + } + + return panic("JSON Schema default is not a JSON value"); + } + + protected defaultValueForClassProperty( + o: ObjectType, + jsonName: string, + ): unknown | undefined { + return this.typeGraph.attributeStore + .tryGet(propertyDefaultValuesTypeAttributeKind, o) + ?.get(jsonName); + } + + private classPropertyDefaultSource( + o: ObjectType, + jsonName: string, + ): Sourcelike { + const value = this.defaultValueForClassProperty(o, jsonName); + if (value === undefined) return []; + + const literal = this.pythonLiteral(value); + if ( + this.pyOptions.features.dataClasses && + !this.pyOptions.pydanticBaseModel && + (Array.isArray(value) || isStringMap(value)) + ) { + return [ + " = ", + this.withImport("dataclasses", "field"), + "(default_factory=lambda: ", + literal, + ")", + ]; + } + + return [" = ", literal]; + } + protected withImport(module: string, name: string): Sourcelike { if (this.pyOptions.features.typeHints || module !== "typing") { // FIXME: This is ugly. We should rather not generate that import in the first @@ -377,8 +456,12 @@ export class PythonRenderer extends ConvenienceRenderer { return; const args: Sourcelike[] = []; - this.forEachClassProperty(t, "none", (name, _, cp) => { - args.push([name, this.typeHint(": ", this.pythonType(cp.type))]); + this.forEachClassProperty(t, "none", (name, jsonName, cp) => { + args.push([ + name, + this.typeHint(": ", this.pythonType(cp.type)), + this.classPropertyDefaultSource(t, jsonName), + ]); }); this.emitBlock( [ @@ -417,21 +500,31 @@ export class PythonRenderer extends ConvenienceRenderer { } protected sortClassProperties( + o: ObjectType, properties: ReadonlyMap, propertyNames: ReadonlyMap, ): ReadonlyMap { + const hasSchemaDefaults = iterableSome(properties.entries(), ([name]) => + this.defaultValueForClassProperty(o, name) !== undefined, + ); if ( + hasSchemaDefaults || this.pyOptions.features.dataClasses || this.pyOptions.pydanticBaseModel ) { - // Properties that get a `" = None"` default must come after all - // properties that don't, or the generated dataclass is invalid. - return mapSortBy(properties, (p: ClassProperty) => - this.classPropertyHasNoneDefault(p) ? 1 : 0, + // Properties that get a default must come after all properties + // that don't, or the generated class is invalid. + return mapSortBy(properties, (p: ClassProperty, jsonName: string) => + this.defaultValueForClassProperty(o, jsonName) !== undefined || + ((this.pyOptions.features.dataClasses || + this.pyOptions.pydanticBaseModel) && + this.classPropertyHasNoneDefault(p)) + ? 1 + : 0, ); } - return super.sortClassProperties(properties, propertyNames); + return super.sortClassProperties(o, properties, propertyNames); } protected emitClass(t: ClassType): void { @@ -451,12 +544,18 @@ export class PythonRenderer extends ConvenienceRenderer { t, "none", (name, jsonName, cp) => { + const defaultValue = + this.defaultValueForClassProperty(t, jsonName); this.emitLine( name, this.typeHint( ": ", - this.pythonType(cp.type, true), + this.pythonType( + cp.type, + defaultValue === undefined, + ), ), + this.classPropertyDefaultSource(t, jsonName), ); this.emitDescription( this.descriptionForClassProperty(t, jsonName), diff --git a/test/inputs/schema/default-values.1.json b/test/inputs/schema/default-values.1.json new file mode 100644 index 0000000000..7b537d49b4 --- /dev/null +++ b/test/inputs/schema/default-values.1.json @@ -0,0 +1,3 @@ +{ + "Name": "Explicit" +} diff --git a/test/inputs/schema/default-values.1.out.defaults.json b/test/inputs/schema/default-values.1.out.defaults.json new file mode 100644 index 0000000000..43f392ebed --- /dev/null +++ b/test/inputs/schema/default-values.1.out.defaults.json @@ -0,0 +1,4 @@ +{ + "Name": "Explicit", + "Greeting": "World" +} diff --git a/test/inputs/schema/default-values.schema b/test/inputs/schema/default-values.schema new file mode 100644 index 0000000000..550860d92c --- /dev/null +++ b/test/inputs/schema/default-values.schema @@ -0,0 +1,20 @@ +{ + "definitions": { + "record:Metadata": { + "type": "object", + "required": ["Name"], + "additionalProperties": false, + "properties": { + "Name": { + "default": "Hello", + "type": "string" + }, + "Greeting": { + "default": "World", + "type": "string" + } + } + } + }, + "$ref": "#/definitions/record:Metadata" +} diff --git a/test/languages.ts b/test/languages.ts index 70fa741ebf..c8357a4474 100644 --- a/test/languages.ts +++ b/test/languages.ts @@ -56,6 +56,7 @@ const skipsMapValueValidation = [ ]; export type LanguageFeature = + | "defaults" | "enum" | "union" | "no-defaults" @@ -272,6 +273,7 @@ export const PythonLanguage: Language = { "enum", "union", "no-defaults", + "defaults", "date-time", "integer-string", "bool-string", From de65d57c8884d95a97480bd7ec70baa5d8599e26 Mon Sep 17 00:00:00 2001 From: Mark Probst Date: Mon, 20 Jul 2026 19:37:27 -0400 Subject: [PATCH 2/9] test: add missing fixture cases for default-values.schema (#3050) Co-Authored-By: Claude --- test/inputs/schema/default-values.1.fail.no-defaults.json | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 test/inputs/schema/default-values.1.fail.no-defaults.json diff --git a/test/inputs/schema/default-values.1.fail.no-defaults.json b/test/inputs/schema/default-values.1.fail.no-defaults.json new file mode 100644 index 0000000000..3cde930ede --- /dev/null +++ b/test/inputs/schema/default-values.1.fail.no-defaults.json @@ -0,0 +1,3 @@ +{ + "Name": 42 +} From eef5348d466f41f1133e74eac43586a9c8c7dbab Mon Sep 17 00:00:00 2001 From: Mark Probst Date: Mon, 20 Jul 2026 20:02:03 -0400 Subject: [PATCH 3/9] style: satisfy Biome lint/format for default-values changes Use Object.hasOwn() instead of Object.prototype.hasOwnProperty.call() and apply Biome formatting to the Python renderers. Co-Authored-By: Claude --- package-lock.json | 2 +- .../src/attributes/DefaultValues.ts | 2 +- .../src/language/Python/JSONPythonRenderer.ts | 6 +++-- .../src/language/Python/PythonRenderer.ts | 23 +++++++++++-------- 4 files changed, 20 insertions(+), 13 deletions(-) diff --git a/package-lock.json b/package-lock.json index 347e305740..d84867d302 100644 --- a/package-lock.json +++ b/package-lock.json @@ -72,7 +72,7 @@ "web-tree-sitter": "^0.26.9" }, "engines": { - "node": ">=20.0.0" + "node": ">=20.19.0" } }, "node_modules/@75lb/deep-merge": { diff --git a/packages/quicktype-core/src/attributes/DefaultValues.ts b/packages/quicktype-core/src/attributes/DefaultValues.ts index 83f0ca1b07..c8fbb45815 100644 --- a/packages/quicktype-core/src/attributes/DefaultValues.ts +++ b/packages/quicktype-core/src/attributes/DefaultValues.ts @@ -66,7 +66,7 @@ export function defaultValuesAttributeProducer( typeof propertySchema === "object" && propertySchema !== null && !Array.isArray(propertySchema) && - Object.prototype.hasOwnProperty.call(propertySchema, "default") + Object.hasOwn(propertySchema, "default") ) { defaults.set( name, diff --git a/packages/quicktype-core/src/language/Python/JSONPythonRenderer.ts b/packages/quicktype-core/src/language/Python/JSONPythonRenderer.ts index 310a63eb7c..9b48be766e 100644 --- a/packages/quicktype-core/src/language/Python/JSONPythonRenderer.ts +++ b/packages/quicktype-core/src/language/Python/JSONPythonRenderer.ts @@ -976,8 +976,10 @@ export class JSONPythonRenderer extends PythonRenderer { const args: Sourcelike[] = []; this.emitLine("assert isinstance(obj, dict)"); this.forEachClassProperty(t, "none", (name, jsonName, cp) => { - const defaultValue = - this.defaultValueForClassProperty(t, jsonName); + const defaultValue = this.defaultValueForClassProperty( + t, + jsonName, + ); const property = { value: defaultValue === undefined diff --git a/packages/quicktype-core/src/language/Python/PythonRenderer.ts b/packages/quicktype-core/src/language/Python/PythonRenderer.ts index 5bac7414b7..e661123609 100644 --- a/packages/quicktype-core/src/language/Python/PythonRenderer.ts +++ b/packages/quicktype-core/src/language/Python/PythonRenderer.ts @@ -504,8 +504,10 @@ export class PythonRenderer extends ConvenienceRenderer { properties: ReadonlyMap, propertyNames: ReadonlyMap, ): ReadonlyMap { - const hasSchemaDefaults = iterableSome(properties.entries(), ([name]) => - this.defaultValueForClassProperty(o, name) !== undefined, + const hasSchemaDefaults = iterableSome( + properties.entries(), + ([name]) => + this.defaultValueForClassProperty(o, name) !== undefined, ); if ( hasSchemaDefaults || @@ -514,13 +516,16 @@ export class PythonRenderer extends ConvenienceRenderer { ) { // Properties that get a default must come after all properties // that don't, or the generated class is invalid. - return mapSortBy(properties, (p: ClassProperty, jsonName: string) => - this.defaultValueForClassProperty(o, jsonName) !== undefined || - ((this.pyOptions.features.dataClasses || - this.pyOptions.pydanticBaseModel) && - this.classPropertyHasNoneDefault(p)) - ? 1 - : 0, + return mapSortBy( + properties, + (p: ClassProperty, jsonName: string) => + this.defaultValueForClassProperty(o, jsonName) !== + undefined || + ((this.pyOptions.features.dataClasses || + this.pyOptions.pydanticBaseModel) && + this.classPropertyHasNoneDefault(p)) + ? 1 + : 0, ); } From de7b2bb1ce485b0af16eb9957aee9b094f6a2621 Mon Sep 17 00:00:00 2001 From: Mark Probst Date: Mon, 20 Jul 2026 20:48:38 -0400 Subject: [PATCH 4/9] fix CI: use hasOwnProperty instead of Object.hasOwn for es6 lib target (#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 --- packages/quicktype-core/src/attributes/DefaultValues.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/quicktype-core/src/attributes/DefaultValues.ts b/packages/quicktype-core/src/attributes/DefaultValues.ts index c8fbb45815..75c2490da0 100644 --- a/packages/quicktype-core/src/attributes/DefaultValues.ts +++ b/packages/quicktype-core/src/attributes/DefaultValues.ts @@ -66,7 +66,8 @@ export function defaultValuesAttributeProducer( typeof propertySchema === "object" && propertySchema !== null && !Array.isArray(propertySchema) && - Object.hasOwn(propertySchema, "default") + // biome-ignore lint/suspicious/noPrototypeBuiltins: Object.hasOwn is not in quicktype-core's es6 lib + Object.prototype.hasOwnProperty.call(propertySchema, "default") ) { defaults.set( name, From 43db7ed9fa901cd6983b26e02239594f8b6b1bfb Mon Sep 17 00:00:00 2001 From: Mark Probst Date: Mon, 20 Jul 2026 21:13:38 -0400 Subject: [PATCH 5/9] fix CI: correct stale enum-order expectation in cjson-enum-default test (#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 --- test/unit/cjson-enum-default.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/unit/cjson-enum-default.test.ts b/test/unit/cjson-enum-default.test.ts index b2c02d60ee..0ecd24cb92 100644 --- a/test/unit/cjson-enum-default.test.ts +++ b/test/unit/cjson-enum-default.test.ts @@ -28,9 +28,9 @@ describe("cJSON enum invalid value", () => { const output = await cJSONOutput(); expect(output).toContain(`enum Subscription { - SUBSCRIPTION_CONFIG = 1, + SUBSCRIPTION_STATE = 1, + SUBSCRIPTION_CONFIG, SUBSCRIPTION_HEARTBEAT, - SUBSCRIPTION_STATE, };`); expect(output).toContain("enum Subscription x = 0;"); }); From 9ab0e8638f6bf4def94c497bd6df310c546ae365 Mon Sep 17 00:00:00 2001 From: Mark Probst Date: Mon, 20 Jul 2026 21:30:45 -0400 Subject: [PATCH 6/9] fix CI: use a type that no language coerces in default-values no-defaults 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 --- test/inputs/schema/default-values.1.fail.no-defaults.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/inputs/schema/default-values.1.fail.no-defaults.json b/test/inputs/schema/default-values.1.fail.no-defaults.json index 3cde930ede..bf8ed2a8fa 100644 --- a/test/inputs/schema/default-values.1.fail.no-defaults.json +++ b/test/inputs/schema/default-values.1.fail.no-defaults.json @@ -1,3 +1,3 @@ { - "Name": 42 + "Name": {} } From c450a61a6f589aa9451720a2e665ac04dd6122ef Mon Sep 17 00:00:00 2001 From: Mark Probst Date: Tue, 21 Jul 2026 10:33:41 -0400 Subject: [PATCH 7/9] fix CI: use a JSON array so Klaxon's Kotlin fixture rejects the no-defaults 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 --- test/inputs/schema/default-values.1.fail.no-defaults.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/inputs/schema/default-values.1.fail.no-defaults.json b/test/inputs/schema/default-values.1.fail.no-defaults.json index bf8ed2a8fa..4d64d54db1 100644 --- a/test/inputs/schema/default-values.1.fail.no-defaults.json +++ b/test/inputs/schema/default-values.1.fail.no-defaults.json @@ -1,3 +1,3 @@ { - "Name": {} + "Name": [] } From 4b122d3ed80477c07a9b2e2c8bd30fa21323d09e Mon Sep 17 00:00:00 2001 From: Mark Probst Date: Tue, 21 Jul 2026 10:45:00 -0400 Subject: [PATCH 8/9] fix CI: skip default-values.schema for Elixir, whose runtime can't validate 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 --- test/languages.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/test/languages.ts b/test/languages.ts index a8943d48b9..5ce706c42f 100644 --- a/test/languages.ts +++ b/test/languages.ts @@ -2239,6 +2239,7 @@ export const ElixirLanguage: Language = { "mutually-recursive.schema", // Struct keys cannot be enforced at runtime in Elixir and their values will just be set to null. + "default-values.schema", "strict-optional.schema", "required.schema", "boolean-subschema.schema", From 41cbf062da0c39462b0e9dc61ef0939457303717 Mon Sep 17 00:00:00 2001 From: Mark Probst Date: Tue, 21 Jul 2026 10:57:37 -0400 Subject: [PATCH 9/9] fix CI: skip default-values.schema for Haskell, whose driver prints null on decode failure (#3050) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- test/languages.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/test/languages.ts b/test/languages.ts index 5ce706c42f..123392f1de 100644 --- a/test/languages.ts +++ b/test/languages.ts @@ -1886,6 +1886,7 @@ export const HaskellLanguage: Language = { ...skipsUntypedUnions, // The test driver encodes the Maybe result, so a failed decode prints // "null" and exits 0 — expected-failure samples cannot be detected. + "default-values.schema", "boolean-subschema.schema", "nested-intersection-union.schema", "prefix-items.schema",