From 8493ff4ccad0acad000c0c5ea9baf713d21ef2b3 Mon Sep 17 00:00:00 2001 From: Mark Probst Date: Sun, 19 Jul 2026 19:47:26 -0400 Subject: [PATCH 01/13] feat(kotlin)!: default framework is now jackson Klaxon has been unmaintained since ~2022 and our own fixture skip-lists cite its open bugs; the Jackson renderer is feature-complete and fully CI-covered. The kotlin/schema-kotlin fixtures now pin framework=klaxon explicitly so the Klaxon renderer keeps end-to-end coverage (kotlin-jackson and kotlinx fixtures cover the other frameworks). Co-Authored-By: Claude Fable 5 --- packages/quicktype-core/src/language/Kotlin/language.ts | 2 +- test/languages.ts | 4 +++- test/unit/just-types-option.test.ts | 4 ++-- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/packages/quicktype-core/src/language/Kotlin/language.ts b/packages/quicktype-core/src/language/Kotlin/language.ts index 28adca237c..528d00dd6b 100644 --- a/packages/quicktype-core/src/language/Kotlin/language.ts +++ b/packages/quicktype-core/src/language/Kotlin/language.ts @@ -33,7 +33,7 @@ export const kotlinOptions = { klaxon: "Klaxon", kotlinx: "KotlinX", } as const, - "klaxon", + "jackson", ), acronymStyle: acronymOption(AcronymStyleOptions.Pascal), packageName: new StringOption("package", "Package", "PACKAGE", "quicktype"), diff --git a/test/languages.ts b/test/languages.ts index b64b21cfca..a94a0d0cff 100644 --- a/test/languages.ts +++ b/test/languages.ts @@ -1278,7 +1278,9 @@ export const KotlinLanguage: Language = { "recursive-union-flattening.schema", ], skipMiscJSON: false, - rendererOptions: {}, + // The default framework is jackson; this fixture deliberately pins + // klaxon so the Klaxon renderer keeps end-to-end coverage. + rendererOptions: { framework: "klaxon" }, quickTestRendererOptions: [], sourceFiles: ["src/language/Kotlin/index.ts"], }; diff --git a/test/unit/just-types-option.test.ts b/test/unit/just-types-option.test.ts index 8ae56e317a..4d3e31766a 100644 --- a/test/unit/just-types-option.test.ts +++ b/test/unit/just-types-option.test.ts @@ -131,8 +131,8 @@ describe("framework defaults", () => { expect(output).toContain("io.circe"); }); - test("Kotlin defaults to Klaxon", async () => { + test("Kotlin defaults to Jackson", async () => { const output = await linesFor("kotlin"); - expect(output).toContain("com.beust.klaxon"); + expect(output).toContain("com.fasterxml.jackson"); }); }); From fa3eb91bda545da8c63f03a65d4f4d612617eda3 Mon Sep 17 00:00:00 2001 From: Mark Probst Date: Sun, 19 Jul 2026 19:47:55 -0400 Subject: [PATCH 02/13] feat(csharp)!: default framework is now SystemTextJson System.Text.Json is the built-in, Microsoft-recommended serializer; Newtonsoft.Json is in maintenance mode. The csharp, schema-csharp, schema-json-csharp, and graphql-csharp fixtures now pin framework=NewtonSoft explicitly so the Newtonsoft renderer keeps its end-to-end coverage (the SystemTextJson fixtures already exist). Co-Authored-By: Claude Fable 5 --- packages/quicktype-core/src/language/CSharp/language.ts | 2 +- test/languages.ts | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/quicktype-core/src/language/CSharp/language.ts b/packages/quicktype-core/src/language/CSharp/language.ts index 0b0c7235f5..2115aac3e8 100644 --- a/packages/quicktype-core/src/language/CSharp/language.ts +++ b/packages/quicktype-core/src/language/CSharp/language.ts @@ -35,7 +35,7 @@ export const cSharpOptions = { NewtonSoft: "NewtonSoft", SystemTextJson: "SystemTextJson", } as const, - "NewtonSoft", + "SystemTextJson", ), useList: new EnumOption( "array-type", diff --git a/test/languages.ts b/test/languages.ts index a94a0d0cff..6d11b24445 100644 --- a/test/languages.ts +++ b/test/languages.ts @@ -114,7 +114,9 @@ export const CSharpLanguage: Language = { skipSchema: [ "top-level-enum.schema", // The code we generate for top-level enums is incompatible with the driver ], - rendererOptions: { "check-required": "true" }, + // The default framework is SystemTextJson; this fixture deliberately + // pins NewtonSoft so the Newtonsoft renderer keeps end-to-end coverage. + rendererOptions: { "check-required": "true", framework: "NewtonSoft" }, quickTestRendererOptions: [ { "array-type": "list" }, { "csharp-version": "5" }, From e2d5790cc8fbe685e36225e9497fddb044803fa1 Mon Sep 17 00:00:00 2001 From: Mark Probst Date: Sun, 19 Jul 2026 19:48:19 -0400 Subject: [PATCH 03/13] feat(cplusplus)!: boost is now off by default C++17 std::optional/std::variant have been universally available since ~2018; requiring Boost by default is a legacy accommodation. CI already compiles the fixture at -std=c++17. The quicktest options now exercise boost=true so the Boost code path keeps coverage. Co-Authored-By: Claude Fable 5 --- packages/quicktype-core/src/language/CPlusPlus/language.ts | 2 +- test/languages.ts | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/quicktype-core/src/language/CPlusPlus/language.ts b/packages/quicktype-core/src/language/CPlusPlus/language.ts index e614e7ec9e..cca4dd10be 100644 --- a/packages/quicktype-core/src/language/CPlusPlus/language.ts +++ b/packages/quicktype-core/src/language/CPlusPlus/language.ts @@ -104,7 +104,7 @@ export const cPlusPlusOptions = { boost: new BooleanOption( "boost", "Require a dependency on boost. Without boost, C++17 is required", - true, + false, ), hideNullOptional: new BooleanOption( "hide-null-optional", diff --git a/test/languages.ts b/test/languages.ts index 6d11b24445..2a1dd4a6fe 100644 --- a/test/languages.ts +++ b/test/languages.ts @@ -721,7 +721,9 @@ export const CPlusPlusLanguage: Language = { { "code-format": "with-struct" }, { wstring: "use-wstring" }, { "const-style": "east-const" }, - { boost: "false" }, + // The default is boost=false (C++17); this keeps the boost code + // path covered. + { boost: "true" }, ], sourceFiles: ["src/language/CPlusPlus/index.ts"], }; From 4e65b12c4ab54b2569610c878d4e46844d197fd6 Mon Sep 17 00:00:00 2001 From: Mark Probst Date: Sun, 19 Jul 2026 19:49:10 -0400 Subject: [PATCH 04/13] feat(rust)!: public fields and Debug/Clone derives by default; remove edition-2018 - visibility now defaults to public: private fields on generated structs can neither be constructed nor read by consumers, and every modern Rust codegen emits pub fields. - derive-debug and derive-clone now default to true, per the Rust API guidelines (C-COMMON-TRAITS); all emitted field types are Clone, so this always compiles. - The vestigial edition-2018 option is deleted: it only toggled between 'use serde::...' and the pre-2018 'extern crate serde_derive;' prelude. The 2018+ form is now unconditional. Passing --edition-2018 is an error now (breaking). A quicktest combo pins the old defaults (visibility=private, no derives) so the legacy output shape keeps fixture coverage. Co-Authored-By: Claude Fable 5 --- .../quicktype-core/src/language/Rust/RustRenderer.ts | 6 +----- packages/quicktype-core/src/language/Rust/language.ts | 7 +++---- test/languages.ts | 9 +++++++-- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/packages/quicktype-core/src/language/Rust/RustRenderer.ts b/packages/quicktype-core/src/language/Rust/RustRenderer.ts index c342f0141d..3024d97d5b 100644 --- a/packages/quicktype-core/src/language/Rust/RustRenderer.ts +++ b/packages/quicktype-core/src/language/Rust/RustRenderer.ts @@ -376,11 +376,7 @@ export class RustRenderer extends ConvenienceRenderer { } this.ensureBlankLine(); - if (this._options.edition2018) { - this.emitLine("use serde::{Serialize, Deserialize};"); - } else { - this.emitLine("extern crate serde_derive;"); - } + this.emitLine("use serde::{Serialize, Deserialize};"); if (this.haveMaps) { this.emitLine("use std::collections::HashMap;"); diff --git a/packages/quicktype-core/src/language/Rust/language.ts b/packages/quicktype-core/src/language/Rust/language.ts index 29030a4994..692980468a 100644 --- a/packages/quicktype-core/src/language/Rust/language.ts +++ b/packages/quicktype-core/src/language/Rust/language.ts @@ -28,10 +28,10 @@ export const rustOptions = { crate: Visibility.Crate, public: Visibility.Public, } as const, - "private", + "public", ), - deriveDebug: new BooleanOption("derive-debug", "Derive Debug impl", false), - deriveClone: new BooleanOption("derive-clone", "Derive Clone impl", false), + deriveDebug: new BooleanOption("derive-debug", "Derive Debug impl", true), + deriveClone: new BooleanOption("derive-clone", "Derive Clone impl", true), derivePartialEq: new BooleanOption( "derive-partial-eq", "Derive PartialEq impl", @@ -42,7 +42,6 @@ export const rustOptions = { "Skip serializing empty Option fields", false, ), - edition2018: new BooleanOption("edition-2018", "Edition 2018", true), leadingComments: new BooleanOption( "leading-comments", "Leading Comments", diff --git a/test/languages.ts b/test/languages.ts index 2a1dd4a6fe..993b51bb6c 100644 --- a/test/languages.ts +++ b/test/languages.ts @@ -305,8 +305,13 @@ export const RustLanguage: Language = { quickTestRendererOptions: [ { density: "dense" }, { visibility: "crate" }, - { visibility: "private" }, - { visibility: "public" }, + // The pre-flip defaults: private fields without Debug/Clone + // derives, kept covered after the defaults changed. + { + visibility: "private", + "derive-debug": "false", + "derive-clone": "false", + }, ], sourceFiles: ["src/language/Rust/index.ts"], }; From 130bd6fe7fcac92fb24bec1850f9cf7676e5de88 Mon Sep 17 00:00:00 2001 From: Mark Probst Date: Sun, 19 Jul 2026 19:49:50 -0400 Subject: [PATCH 05/13] feat(typescript)!: prefer-unions is now on by default The ecosystem has turned on TypeScript enum syntax: Node's default type stripping and tsc --erasableSyntaxOnly both hard-error on it. Enums now render as unions of string literals by default; --no-prefer-unions restores the old output. Runtime typechecking of enum values is unaffected (the typeMap lists case values either way), so the schema fixtures' enum fail-samples still fail as required. Flow always rendered enums as string-literal unions and ignores this option; its output is unchanged. quicktests pin prefer-unions=false for typescript (covering the enum code path) and flow. Co-Authored-By: Claude Fable 5 --- .../quicktype-core/src/language/TypeScriptFlow/language.ts | 2 +- test/languages.ts | 7 +++++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/packages/quicktype-core/src/language/TypeScriptFlow/language.ts b/packages/quicktype-core/src/language/TypeScriptFlow/language.ts index d7f0f208ba..78d9b38f0b 100644 --- a/packages/quicktype-core/src/language/TypeScriptFlow/language.ts +++ b/packages/quicktype-core/src/language/TypeScriptFlow/language.ts @@ -32,7 +32,7 @@ export const tsFlowOptions = { preferUnions: new BooleanOption( "prefer-unions", "Use union type instead of enum", - false, + true, ), preferTypes: new BooleanOption( "prefer-types", diff --git a/test/languages.ts b/test/languages.ts index 993b51bb6c..7e36ecad1a 100644 --- a/test/languages.ts +++ b/test/languages.ts @@ -963,6 +963,9 @@ export const TypeScriptLanguage: Language = { { "acronym-style": "pascal" }, { converters: "all-objects" }, { readonly: "true" }, + // The default is prefer-unions=true; this keeps the TypeScript + // enum code path covered. + { "prefer-unions": "false" }, ], sourceFiles: ["src/language/TypeScript/index.ts"], }; @@ -1042,6 +1045,10 @@ export const FlowLanguage: Language = { { "runtime-typecheck": "false" }, { "runtime-typecheck-ignore-unknown-properties": "true" }, { "nice-property-names": "true" }, + // Flow always renders enums as unions of string literals, so + // this only asserts that the flipped default stays a no-op for + // Flow output. + { "prefer-unions": "false" }, ], sourceFiles: ["src/language/Flow/index.ts"], }; From dd015225e201d7be3e9ed995308795b49c244806 Mon Sep 17 00:00:00 2001 From: Mark Probst Date: Sun, 19 Jul 2026 19:50:31 -0400 Subject: [PATCH 06/13] feat(java)!: arrays now render as List by default List is the universal Java DTO idiom (Effective Java, OpenAPI Generator, jsonschema2pojo); raw arrays fight equals/hashCode and generics. The quicktest entries for the java, java-datetime-legacy, and java-lombok fixtures now pin array-type=array so the T[] code path keeps coverage. Co-Authored-By: Claude Fable 5 --- packages/quicktype-core/src/language/Java/language.ts | 2 +- test/languages.ts | 8 +++++--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/packages/quicktype-core/src/language/Java/language.ts b/packages/quicktype-core/src/language/Java/language.ts index fac9e16b87..87d5166219 100644 --- a/packages/quicktype-core/src/language/Java/language.ts +++ b/packages/quicktype-core/src/language/Java/language.ts @@ -22,7 +22,7 @@ export const javaOptions = { "array-type", "Use T[] or List", { array: false, list: true } as const, - "array", + "list", ), justTypes: new BooleanOption("just-types", "Plain types only", false), dateTimeProvider: new EnumOption( diff --git a/test/languages.ts b/test/languages.ts index 7e36ecad1a..c16cd17993 100644 --- a/test/languages.ts +++ b/test/languages.ts @@ -200,7 +200,9 @@ export const JavaLanguage: Language = { skipMiscJSON: false, skipSchema: ["keyword-unions.schema"], // generates classes with names that are case-insensitively equal rendererOptions: {}, - quickTestRendererOptions: [{ "array-type": "list" }], + // The default is array-type=list; this keeps the T[] code path + // covered. + quickTestRendererOptions: [{ "array-type": "array" }], sourceFiles: ["src/language/Java/index.ts"], }; @@ -217,13 +219,13 @@ export const JavaLanguageWithLegacyDateTime: Language = { ], skipMiscJSON: true, // Handles edge cases differently and does not allow optional milliseconds. rendererOptions: { "datetime-provider": "legacy" }, - quickTestRendererOptions: [{ "array-type": "list" }], + quickTestRendererOptions: [{ "array-type": "array" }], }; export const JavaLanguageWithLombok: Language = { ...JavaLanguage, base: "test/fixtures/java-lombok", - quickTestRendererOptions: [{ "array-type": "list", lombok: "true" }], + quickTestRendererOptions: [{ "array-type": "array", lombok: "true" }], }; export const PythonLanguage: Language = { From 06f7556a439d0a6837d3feffaf9af1548770b8c0 Mon Sep 17 00:00:00 2001 From: Mark Probst Date: Sun, 19 Jul 2026 19:52:31 -0400 Subject: [PATCH 07/13] feat(dart)!: final properties by default; remove null-safety option - final-props now defaults to true: Effective Dart and all current data-class idioms (freezed, @immutable culture) use final fields. A quicktest pins final-props=false so the mutable-property path keeps coverage. - The null-safety option is deleted: null-safe output is the only choice that compiles under Dart 3, so the non-null-safe branches were dead weight. Null-safe rendering is now unconditional; passing --null-safety/--no-null-safety is an error now (breaking). Co-Authored-By: Claude Fable 5 --- .../src/language/Dart/DartRenderer.ts | 45 +++++-------------- .../src/language/Dart/language.ts | 3 +- test/languages.ts | 4 +- 3 files changed, 15 insertions(+), 37 deletions(-) diff --git a/packages/quicktype-core/src/language/Dart/DartRenderer.ts b/packages/quicktype-core/src/language/Dart/DartRenderer.ts index f11f77f6f3..8a46bd7577 100644 --- a/packages/quicktype-core/src/language/Dart/DartRenderer.ts +++ b/packages/quicktype-core/src/language/Dart/DartRenderer.ts @@ -266,9 +266,7 @@ export class DartRenderer extends ConvenienceRenderer { ): Sourcelike { const nullable = forceNullable || - (this._options.nullSafety && - t.isNullable && - !this._options.requiredProperties); + (t.isNullable && !this._options.requiredProperties); const withNullable = (s: Sourcelike): Sourcelike => nullable ? [s, "?"] : s; return matchType( @@ -321,11 +319,7 @@ export class DartRenderer extends ConvenienceRenderer { list: Sourcelike, mapper: Sourcelike, ): Sourcelike { - if ( - this._options.nullSafety && - isNullable && - !this._options.requiredProperties - ) { + if (isNullable && !this._options.requiredProperties) { return [ list, " == null ? [] : ", @@ -356,11 +350,7 @@ export class DartRenderer extends ConvenienceRenderer { map: Sourcelike, valueMapper: Sourcelike, ): Sourcelike { - if ( - this._options.nullSafety && - isNullable && - !this._options.requiredProperties - ) { + if (isNullable && !this._options.requiredProperties) { return [ "Map.from(", map, @@ -388,11 +378,7 @@ export class DartRenderer extends ConvenienceRenderer { classType: ClassType, dynamic: Sourcelike, ): Sourcelike { - if ( - this._options.nullSafety && - isNullable && - !this._options.requiredProperties - ) { + if (isNullable && !this._options.requiredProperties) { return [ dynamic, " == null ? null : ", @@ -431,10 +417,7 @@ export class DartRenderer extends ConvenienceRenderer { (_nullType) => dynamic, // FIXME: check null (_boolType) => dynamic, (_integerType) => dynamic, - (_doubleType) => [ - dynamic, - this._options.nullSafety ? "?.toDouble()" : ".toDouble()", - ], + (_doubleType) => [dynamic, "?.toDouble()"], (_stringType) => dynamic, (arrayType) => this.mapList( @@ -469,8 +452,7 @@ export class DartRenderer extends ConvenienceRenderer { defined(this._enumValues.get(enumType)), ".map[", dynamic, - this._options.nullSafety && - (!isNullable || this._options.requiredProperties) + !isNullable || this._options.requiredProperties ? "]!" : "]", ]; @@ -493,8 +475,7 @@ export class DartRenderer extends ConvenienceRenderer { case "date": if ( (transformedStringType.isNullable || isNullable) && - !this._options.requiredProperties && - this._options.nullSafety + !this._options.requiredProperties ) { return [ dynamic, @@ -544,7 +525,6 @@ export class DartRenderer extends ConvenienceRenderer { ), (_classType) => { if ( - this._options.nullSafety && (_classType.isNullable || isNullable) && !this._options.requiredProperties ) { @@ -588,7 +568,6 @@ export class DartRenderer extends ConvenienceRenderer { switch (transformedStringType.kind) { case "date-time": if ( - this._options.nullSafety && !this._options.requiredProperties && (transformedStringType.isNullable || isNullable) ) { @@ -598,7 +577,6 @@ export class DartRenderer extends ConvenienceRenderer { return [dynamic, ".toIso8601String()"]; case "date": if ( - this._options.nullSafety && !this._options.requiredProperties && (transformedStringType.isNullable || isNullable) ) { @@ -642,8 +620,8 @@ export class DartRenderer extends ConvenienceRenderer { this.forEachClassProperty(c, "none", (name, _, prop) => { const required = this._options.requiredProperties || - (this._options.nullSafety && - (!prop.type.isNullable || !prop.isOptional)); + !prop.type.isNullable || + !prop.isOptional; this.emitLine(required ? "required " : "", "this.", name, ","); }); }); @@ -869,9 +847,8 @@ export class DartRenderer extends ConvenienceRenderer { const required = this._options.requiredProperties || - (this._options.nullSafety && - (!prop.type.isNullable || - !prop.isOptional)); + !prop.type.isNullable || + !prop.isOptional; if (this._options.useJsonAnnotation) { this.classPropertyCounter++; this.emitLine( diff --git a/packages/quicktype-core/src/language/Dart/language.ts b/packages/quicktype-core/src/language/Dart/language.ts index f32e85868e..167b061d71 100644 --- a/packages/quicktype-core/src/language/Dart/language.ts +++ b/packages/quicktype-core/src/language/Dart/language.ts @@ -15,7 +15,6 @@ import type { LanguageName, RendererOptions } from "../../types.js"; import { DartRenderer } from "./DartRenderer.js"; export const dartOptions = { - nullSafety: new BooleanOption("null-safety", "Null Safety", true), justTypes: new BooleanOption("just-types", "Types only", false), codersInClass: new BooleanOption( "coders-in-class", @@ -36,7 +35,7 @@ export const dartOptions = { finalProperties: new BooleanOption( "final-props", "Make all properties final", - false, + true, ), generateCopyWith: new BooleanOption( "copy-with", diff --git a/test/languages.ts b/test/languages.ts index c16cd17993..ac2692e2ab 100644 --- a/test/languages.ts +++ b/test/languages.ts @@ -1584,7 +1584,9 @@ export const DartLanguage: Language = { ], skipMiscJSON: true, rendererOptions: {}, - quickTestRendererOptions: [], + // The default is final-props=true; this keeps the mutable-property + // code path covered. + quickTestRendererOptions: [{ "final-props": "false" }], sourceFiles: ["src/language/Dart/index.ts"], }; From 120c95872fbad5c83bae3c61a175e8bd3ee2279d Mon Sep 17 00:00:00 2001 From: Mark Probst Date: Sun, 19 Jul 2026 19:53:07 -0400 Subject: [PATCH 08/13] feat(swift)!: density now defaults to normal; remove swift-5-support - density=normal (one property per line, explicit CodingKeys) is what swift-format, SwiftFormat, and the Google Swift style guide produce; merged declarations read as a codegen quirk. Round-tripping is identical. The dense path stays covered by the existing density=dense quicktest; the now-redundant density=normal quicktest is dropped. - The swift-5-support option is deleted: the renderer never read it, so it has been a no-op for years. Passing --swift-5-support is an error now (breaking). Co-Authored-By: Claude Fable 5 --- packages/quicktype-core/src/language/Swift/language.ts | 7 +------ test/languages.ts | 3 ++- 2 files changed, 3 insertions(+), 7 deletions(-) diff --git a/packages/quicktype-core/src/language/Swift/language.ts b/packages/quicktype-core/src/language/Swift/language.ts index 1e6a53b493..7e9fecf155 100644 --- a/packages/quicktype-core/src/language/Swift/language.ts +++ b/packages/quicktype-core/src/language/Swift/language.ts @@ -72,7 +72,7 @@ export const swiftOptions = { dense: true, normal: false, } as const, - "dense", + "normal", "secondary", ), linux: new BooleanOption( @@ -91,11 +91,6 @@ export const swiftOptions = { "If no matching case is found enum value is set to null", false, ), - swift5Support: new BooleanOption( - "swift-5-support", - "Renders output in a Swift 5 compatible mode", - false, - ), sendable: new BooleanOption( "sendable", "Mark generated models as Sendable", diff --git a/test/languages.ts b/test/languages.ts index ac2692e2ab..22ac96cd4d 100644 --- a/test/languages.ts +++ b/test/languages.ts @@ -875,8 +875,9 @@ export const SwiftLanguage: Language = { "simple-object.json", { "struct-or-class": "class", "final-classes": "true" }, ], + // The default is density=normal; this keeps the dense code path + // covered. { density: "dense" }, - { density: "normal" }, { "access-level": "internal" }, { "access-level": "public" }, { protocol: "equatable" }, From dd91f6f99de6c025fc790f6250d97c7e292a4318 Mon Sep 17 00:00:00 2001 From: Mark Probst Date: Sun, 19 Jul 2026 19:53:36 -0400 Subject: [PATCH 09/13] feat(haskell)!: arrays now render as lists by default Hand-written aeson records overwhelmingly use [a]; the Vector default was inherited from the Elm renderer and forces an extra import. The quicktest entry now pins array-type=array so the Vector code path keeps coverage. Co-Authored-By: Claude Fable 5 --- packages/quicktype-core/src/language/Haskell/language.ts | 2 +- test/languages.ts | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/quicktype-core/src/language/Haskell/language.ts b/packages/quicktype-core/src/language/Haskell/language.ts index 422adbd528..6d764ae766 100644 --- a/packages/quicktype-core/src/language/Haskell/language.ts +++ b/packages/quicktype-core/src/language/Haskell/language.ts @@ -19,7 +19,7 @@ export const haskellOptions = { array: false, list: true, } as const, - "array", + "list", ), moduleName: new StringOption( "module", diff --git a/test/languages.ts b/test/languages.ts index 22ac96cd4d..b07495510c 100644 --- a/test/languages.ts +++ b/test/languages.ts @@ -1731,7 +1731,9 @@ export const HaskellLanguage: Language = { "required-non-properties.schema", ], rendererOptions: {}, - quickTestRendererOptions: [{ "array-type": "list" }], + // The default is array-type=list; this keeps the Vector code path + // covered. + quickTestRendererOptions: [{ "array-type": "array" }], sourceFiles: ["src/language/Haskell/index.ts"], }; From 3b2cb1b758f6fed40f4902e24d9ee9ab5c8cca88 Mon Sep 17 00:00:00 2001 From: Mark Probst Date: Sun, 19 Jul 2026 20:34:39 -0400 Subject: [PATCH 10/13] fix(cplusplus): preserve JSON null in std::optional/shared_ptr deserialization MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The emitted adl_serializer for optionals handled null by calling the optional *factory* with no arguments. That is only correct in boost mode, where the factory is boost::optional and produces an empty optional. In boost-free mode std::make_optional() wraps a default-constructed T, and std::make_shared() (both modes) points at a default-constructed T — so a JSON null inside a union or array came back as 0/""/{} on round-trip. Null now maps to an empty optType(). This is a pre-existing bug in the boost=false option; flipping the default to boost=false in this branch merely exposed it, because the old {boost:"false"} quicktest never actually ran: quicktests without a pinned input run against combinations[1-4].json, which are all in the C++ fixture's skipJSON. The boost quicktests are therefore now pinned to unions.json (nulls inside unions, where the boost and std code paths differ) and pokedex.json so they really execute. Verified locally: QUICKTEST=true FIXTURE=cplusplus passes end to end (63 tests), with the boost=true quicktests compiled against Boost 1.84 headers; unions.json round-trips byte-identically in both modes. Co-Authored-By: Claude Fable 5 --- .../src/language/CPlusPlus/CPlusPlusRenderer.ts | 8 +++++++- test/languages.ts | 9 +++++++-- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/packages/quicktype-core/src/language/CPlusPlus/CPlusPlusRenderer.ts b/packages/quicktype-core/src/language/CPlusPlus/CPlusPlusRenderer.ts index 289d2652ae..ec99e8029b 100644 --- a/packages/quicktype-core/src/language/CPlusPlus/CPlusPlusRenderer.ts +++ b/packages/quicktype-core/src/language/CPlusPlus/CPlusPlusRenderer.ts @@ -2144,8 +2144,14 @@ export class CPlusPlusRenderer extends ConvenienceRenderer { ], false, () => { + // A JSON null must become an *empty* + // optional. Only `optType()` guarantees + // that: the factory would wrap a + // default-constructed T (std::make_optional + // and std::make_shared both do), turning + // null into 0/""/{} on round-trip. this.emitLine( - `if (j.is_null()) return ${factory}(); else return ${factory}(j.get());`, + `if (j.is_null()) return ${optType}(); else return ${factory}(j.get());`, ); }, ); diff --git a/test/languages.ts b/test/languages.ts index b07495510c..9386bba84c 100644 --- a/test/languages.ts +++ b/test/languages.ts @@ -729,8 +729,13 @@ export const CPlusPlusLanguage: Language = { { wstring: "use-wstring" }, { "const-style": "east-const" }, // The default is boost=false (C++17); this keeps the boost code - // path covered. - { boost: "true" }, + // path covered. Pinned to specific inputs because the default + // quicktest inputs (combinations[1-4].json) are all in this + // fixture's skipJSON, so plain-options quicktests never run for + // C++. unions.json exercises nulls inside unions, where the + // boost and std optional/variant code paths differ. + ["unions.json", { boost: "true" }], + ["pokedex.json", { boost: "true" }], ], sourceFiles: ["src/language/CPlusPlus/index.ts"], }; From e78765ae0535be12ba71f6b20d7337fdb7bc5b3b Mon Sep 17 00:00:00 2001 From: Mark Probst Date: Mon, 20 Jul 2026 08:00:50 -0400 Subject: [PATCH 11/13] fix(java): carry element types in union list deserialization MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit In list mode, the union deserializer read JSON arrays with jsonParser.readValueAs(new TypeReference() {}) — a raw List, so Jackson skipped element-type checking entirely and schema-invalid inputs (e.g. a string array where List is expected) deserialized successfully instead of being rejected. The TypeReference now carries the full generic type (javaType instead of javaTypeWithoutGenerics), e.g. TypeReference>. This is a pre-existing bug in the array-type=list option; flipping the default to list in this branch merely exposed it, via the implicit-class-array-union.schema expected-failure sample in the schema-java fixture (its .fail.union.json no longer failed). Verified by running the generated code against Jackson 2.17: all five valid samples round-trip, both fail samples now exit nonzero, and the old code demonstrably accepted the bad input. A corpus-wide sweep of all schema and JSON inputs in list mode shows every emitted TypeReference now carries its element type; union-heavy inputs (unions.json, combinations1-4, php-mixed-union, optional-union, union-constructor-clash) round-trip within the harness' tolerances. (Union members of map type still use raw readValueAs(Map.class) — that looseness is mode-independent and pre-existing in both array and list modes; left as a follow-up.) Co-Authored-By: Claude Fable 5 --- .../src/language/Java/JavaJacksonRenderer.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/packages/quicktype-core/src/language/Java/JavaJacksonRenderer.ts b/packages/quicktype-core/src/language/Java/JavaJacksonRenderer.ts index 673e44ad1f..15c24bebed 100644 --- a/packages/quicktype-core/src/language/Java/JavaJacksonRenderer.ts +++ b/packages/quicktype-core/src/language/Java/JavaJacksonRenderer.ts @@ -206,11 +206,16 @@ export class JacksonRenderer extends JavaRenderer { const { fieldName } = this.unionField(u, t); const rendered = this.javaTypeWithoutGenerics(true, t); if (this._options.useList && t instanceof ArrayType) { + // The TypeReference must carry the full generic type: + // a raw `TypeReference` would make Jackson accept + // any element type, so schema-invalid inputs (which the + // expected-failure fixtures rely on rejecting) would + // deserialize successfully. this.emitLine( "value.", fieldName, " = jsonParser.readValueAs(new TypeReference<", - rendered, + this.javaType(true, t), ">() {});", ); } else if ( From 6285309c8080986ade100d9c3d44aa10f05815ad Mon Sep 17 00:00:00 2001 From: Mark Probst Date: Mon, 20 Jul 2026 08:37:27 -0400 Subject: [PATCH 12/13] feat(csharp)!: default csharp-version is now 8 With #2694 merged, C# 8 output with nullable reference types becomes the out-of-box experience, pairing with the SystemTextJson default. Both csharp fixtures now pin csharp-version=5 and =6 in their quicktests (replacing the now-redundant =8 rows), so all three language-version code paths stay covered. Promoting v8 to the base schema-fixture runs put Newtonsoft v8 over the full schema corpus for the first time (quicktests only cover the JSON fixtures) and exposed a gap in #2694's NRT pragma set: the emitted constraint-check and string-transformer helpers produce CS8602, CS8604, and CS8625 warnings under '#nullable enable', which 'dotnet run' prints to stdout ahead of the JSON, breaking the fixture comparison. Those three codes are now suppressed alongside the existing CS8618/CS8601/CS8603(/CS8765) pragmas; a corpus-wide compile scan of every schema under both frameworks confirms no other warning codes remain. Suppressing CS8602 also fixes the exact issue behind three schema-csharp-SystemTextJson skips (minmaxlength, optional-constraints, optional-const-ref), so those schemas are un-skipped. Verified locally with dotnet SDK 8.0.423: QUICKTEST runs of csharp, csharp-SystemTextJson, schema-csharp, and schema-csharp-SystemTextJson all pass end to end (272 tests), with every quicktest row confirmed executing. Co-Authored-By: Claude Fable 5 --- .../CSharp/NewtonSoftCSharpRenderer.ts | 9 +++++++++ .../CSharp/SystemTextJsonCSharpRenderer.ts | 4 ++++ .../src/language/CSharp/language.ts | 2 +- test/languages.ts | 19 ++++++++++++++----- 4 files changed, 28 insertions(+), 6 deletions(-) diff --git a/packages/quicktype-core/src/language/CSharp/NewtonSoftCSharpRenderer.ts b/packages/quicktype-core/src/language/CSharp/NewtonSoftCSharpRenderer.ts index 1993f6e135..b4aebaf7a2 100644 --- a/packages/quicktype-core/src/language/CSharp/NewtonSoftCSharpRenderer.ts +++ b/packages/quicktype-core/src/language/CSharp/NewtonSoftCSharpRenderer.ts @@ -197,7 +197,10 @@ export class NewtonsoftCSharpRenderer extends CSharpRenderer { this.emitLine("#pragma warning restore CS8618"); this.emitLine("#pragma warning restore CS8601"); + this.emitLine("#pragma warning restore CS8602"); this.emitLine("#pragma warning restore CS8603"); + this.emitLine("#pragma warning restore CS8604"); + this.emitLine("#pragma warning restore CS8625"); this.emitLine("#pragma warning restore CS8765"); } @@ -239,7 +242,13 @@ export class NewtonsoftCSharpRenderer extends CSharpRenderer { this.emitLine("#nullable enable"); this.emitLine("#pragma warning disable CS8618"); this.emitLine("#pragma warning disable CS8601"); + // CS8602/CS8604/CS8625: the emitted constraint-check and + // string-transformer helpers dereference and pass around + // Deserialize() results, which are nullable under NRT. + this.emitLine("#pragma warning disable CS8602"); this.emitLine("#pragma warning disable CS8603"); + this.emitLine("#pragma warning disable CS8604"); + this.emitLine("#pragma warning disable CS8625"); this.emitLine("#pragma warning disable CS8765"); } } diff --git a/packages/quicktype-core/src/language/CSharp/SystemTextJsonCSharpRenderer.ts b/packages/quicktype-core/src/language/CSharp/SystemTextJsonCSharpRenderer.ts index e64a915dcb..f3eccc8df4 100644 --- a/packages/quicktype-core/src/language/CSharp/SystemTextJsonCSharpRenderer.ts +++ b/packages/quicktype-core/src/language/CSharp/SystemTextJsonCSharpRenderer.ts @@ -196,6 +196,7 @@ export class SystemTextJsonCSharpRenderer extends CSharpRenderer { this.emitLine("#pragma warning restore CS8618"); this.emitLine("#pragma warning restore CS8601"); + this.emitLine("#pragma warning restore CS8602"); this.emitLine("#pragma warning restore CS8603"); } @@ -237,6 +238,9 @@ export class SystemTextJsonCSharpRenderer extends CSharpRenderer { this.emitLine("#nullable enable"); this.emitLine("#pragma warning disable CS8618"); this.emitLine("#pragma warning disable CS8601"); + // CS8602: the emitted constraint-check converters dereference + // Deserialize() results, which are nullable under NRT. + this.emitLine("#pragma warning disable CS8602"); this.emitLine("#pragma warning disable CS8603"); } diff --git a/packages/quicktype-core/src/language/CSharp/language.ts b/packages/quicktype-core/src/language/CSharp/language.ts index 48f7473ea7..5501853064 100644 --- a/packages/quicktype-core/src/language/CSharp/language.ts +++ b/packages/quicktype-core/src/language/CSharp/language.ts @@ -71,7 +71,7 @@ export const cSharpOptions = { "6": 6, "8": 8, } as const, - "6", + "8", "secondary", ), virtual: new BooleanOption("virtual", "Generate virtual properties", false), diff --git a/test/languages.ts b/test/languages.ts index b8b5c05183..48fc658fb7 100644 --- a/test/languages.ts +++ b/test/languages.ts @@ -119,8 +119,10 @@ export const CSharpLanguage: Language = { rendererOptions: { "check-required": "true", framework: "NewtonSoft" }, quickTestRendererOptions: [ { "array-type": "list" }, + // The default is csharp-version=8; these keep the older + // language-version code paths covered. { "csharp-version": "5" }, - { "csharp-version": "8" }, + { "csharp-version": "6" }, { density: "dense" }, { "number-type": "decimal" }, { "any-type": "dynamic" }, @@ -160,15 +162,22 @@ export const CSharpLanguageSystemTextJson: Language = { // The following skips are pre-existing System.Text.Json renderer issues, // found when first enabling the schema fixture for this language: "keyword-unions.schema", // a property named "JsonSerializer" collides with System.Text.Json.JsonSerializer: CS0120 - "minmaxlength.schema", // generated converter triggers CS8602 warnings, which "dotnet run" prints to stdout, breaking the JSON comparison - "optional-constraints.schema", // same CS8602 stdout issue; also min/max on integers and pattern on optional strings aren't checked, so expected-failure samples don't fail - "optional-const-ref.schema", // same CS8602 stdout issue; also min/max on integers isn't checked, so the expected-failure sample doesn't fail + // minmaxlength.schema, optional-constraints.schema, and + // optional-const-ref.schema used to be skipped here because the + // generated converters triggered CS8602 warnings, which "dotnet + // run" prints to stdout, breaking the JSON comparison. The + // generated code now suppresses CS8602 alongside the other NRT + // pragmas, so they run. (Their .fail..json samples are + // not exercised because this fixture doesn't declare the minmax, + // minmaxlength, or pattern features.) ], rendererOptions: { "check-required": "true", framework: "SystemTextJson" }, quickTestRendererOptions: [ { "array-type": "list" }, + // The default is csharp-version=8; these keep the older + // language-version code paths covered. + { "csharp-version": "5" }, { "csharp-version": "6" }, - { "csharp-version": "8" }, { density: "dense" }, { "number-type": "decimal" }, { "any-type": "dynamic" }, From 0926136b13ea17a9714ba36a04a3fca978c17a46 Mon Sep 17 00:00:00 2001 From: Mark Probst Date: Mon, 20 Jul 2026 08:53:42 -0400 Subject: [PATCH 13/13] fix(csharp): never double the nullable annotation on optional union properties MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit For an optional property whose type is itself a nullable union (e.g. GraphQL's nullable String, which infers as string | null), propertyDefinition calls nullableCSType on the union, whose csType rendering already resolves the union through nullableCSType and appends '?'. At csharp-version 8 the outer call then appended a second one, emitting 'public string?? Name' — invalid C# (CS1519). The same path produced 'long??' for optional nullable value-type unions at any version. nullableCSType now unwraps a nullable union before rendering, so the annotation is applied exactly once. This is a pre-existing bug in the csharp-version=8 option: the GraphQL fixtures run base options only, so flipping the default to 8 put v8 GraphQL output in CI for the first time (caught by graphql-csharp on github5.graphql). Verified locally with dotnet SDK 8: a corpus-wide sweep (all GraphQL, schema, and JSON inputs under both frameworks, 242 generated files) contains no doubled '?' in any type position, a representative set compiles clean, and the full CI fixture set - csharp, schema-csharp, schema-json-csharp, graphql-csharp, csharp-SystemTextJson, schema-csharp-SystemTextJson - passes end to end (326 tests). Co-Authored-By: Claude Fable 5 --- .../src/language/CSharp/CSharpRenderer.ts | 22 ++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/packages/quicktype-core/src/language/CSharp/CSharpRenderer.ts b/packages/quicktype-core/src/language/CSharp/CSharpRenderer.ts index 3e331a310b..482cb14f71 100644 --- a/packages/quicktype-core/src/language/CSharp/CSharpRenderer.ts +++ b/packages/quicktype-core/src/language/CSharp/CSharpRenderer.ts @@ -15,11 +15,11 @@ import { type Sourcelike, maybeAnnotated } from "../../Source.js"; import { assert } from "../../support/Support.js"; import type { TargetLanguage } from "../../TargetLanguage.js"; import { followTargetType } from "../../Transformers.js"; -import type { - ClassProperty, - ClassType, - EnumType, - Type, +import { + type ClassProperty, + type ClassType, + type EnumType, + type Type, UnionType, } from "../../Type/index.js"; import { @@ -187,6 +187,18 @@ export class CSharpRenderer extends ConvenienceRenderer { withIssues = false, ): Sourcelike { t = followTargetType(t); + // A nullable union already renders with its own "?" through + // csType's union case; unwrap it so the annotation is applied + // exactly once. Without this, an optional property whose type + // is e.g. `string | null` would render as `string??` at C# 8 + // (and a nullable value-type union as `long??` at any version). + if (t instanceof UnionType) { + const nullable = nullableFromUnion(t); + if (nullable !== null) { + t = followTargetType(nullable); + } + } + const csType = this.csType(t, follow, withIssues); if (isValueType(t) || this._csOptions.version >= 8) { return [csType, "?"];