From 03068011e79492b5c6e55a37e8a7e2439b23f0cc Mon Sep 17 00:00:00 2001 From: Simon Parten Date: Fri, 14 Apr 2023 13:35:57 +0200 Subject: [PATCH 01/31] This seems to work up to problems with the ordering of fields --- .../quicktype-core/src/language/Scala3.ts | 415 +++++++++++++----- test/fixtures/scala3/run.sh | 2 +- test/languages.ts | 85 ++++ 3 files changed, 400 insertions(+), 102 deletions(-) diff --git a/packages/quicktype-core/src/language/Scala3.ts b/packages/quicktype-core/src/language/Scala3.ts index 489749ceae..762912ab28 100644 --- a/packages/quicktype-core/src/language/Scala3.ts +++ b/packages/quicktype-core/src/language/Scala3.ts @@ -13,22 +13,13 @@ import { isNumeric, legalizeCharacters, splitIntoWords - } from "../support/Strings"; import { assertNever } from "../support/Support"; import { TargetLanguage } from "../TargetLanguage"; -import { - ArrayType, - ClassProperty, - ClassType, - EnumType, - MapType, - ObjectType, - Type, - UnionType -} from "../Type"; +import { ArrayType, ClassProperty, ClassType, EnumType, MapType, ObjectType, Type, UnionType } from "../Type"; import { matchType, nullableFromUnion, removeNullFromUnion } from "../TypeUtils"; import { RenderContext } from "../Renderer"; +import { json } from "stream/consumers"; export enum Framework { None, @@ -37,18 +28,22 @@ export enum Framework { } export const scala3Options = { - framework: new EnumOption("framework", "Serialization framework", + framework: new EnumOption( + "framework", + "Serialization framework", [ ["just-types", Framework.None], ["circe", Framework.Circe], - ["upickle", Framework.Upickle], - ] - , undefined), + ["upickle", Framework.Upickle] + ], + undefined + ), packageName: new StringOption("package", "Package", "PACKAGE", "quicktype") }; // Use backticks for param names with symbols const invalidSymbols = [ + "?", ":", "-", "+", @@ -135,13 +130,17 @@ const keywords = [ "Enum" ]; - /** * Check if given parameter name should be wrapped in a backtick * @param paramName */ const shouldAddBacktick = (paramName: string): boolean => { - return keywords.some(s => paramName === s) || invalidSymbols.some(s => paramName.includes(s)) || !isNaN(+parseFloat(paramName)) || !isNaN(parseInt(paramName.charAt(0))); + return ( + keywords.some(s => paramName === s) || + invalidSymbols.some(s => paramName.includes(s)) || + !isNaN(+parseFloat(paramName)) || + !isNaN(parseInt(paramName.charAt(0))) + ); }; const wrapOption = (s: string, optional: boolean): string => { @@ -248,10 +247,10 @@ export class Scala3Renderer extends ConvenienceRenderer { delimiter === "curly" ? ["{", "}"] : delimiter === "paren" - ? ["(", ")"] - : delimiter === "none" - ? ["", ""] - : ["{", "})"]; + ? ["(", ")"] + : delimiter === "none" + ? ["", ""] + : ["{", "})"]; this.emitLine(line, " ", open); this.indent(f); this.emitLine(close); @@ -393,29 +392,37 @@ export class Scala3Renderer extends ConvenienceRenderer { } protected emitClassDefinitionMethods() { - this.emitLine(")"); + this.emitLine(")"); } protected emitEnumDefinition(e: EnumType, enumName: Name): void { + console.log("vanilla"); this.emitDescription(this.descriptionForType(e)); this.emitBlock( ["enum ", enumName, " : "], () => { let count = e.cases.size; - if (count > 0) { this.emitItem("\t case ") }; + if (count > 0) { + this.emitItem("\t case "); + } this.forEachEnumCase(e, "none", (name, jsonName) => { + console.log(jsonName); if (!(jsonName == "")) { const backticks = shouldAddBacktick(jsonName) || jsonName.includes(" ") || - !isNaN(parseInt(jsonName.charAt(0))) - if (backticks) { this.emitItem("`") } + !isNaN(parseInt(jsonName.charAt(0))); + if (backticks) { + this.emitItem("`"); + } this.emitItemOnce([name]); - if (backticks) { this.emitItem("`") } + if (backticks) { + this.emitItem("`"); + } if (--count > 0) this.emitItem([","]); } else { - --count + --count; } }); }, @@ -433,7 +440,7 @@ export class Scala3Renderer extends ConvenienceRenderer { this.emitDescription(this.descriptionForType(u)); const [maybeNull, nonNulls] = removeNullFromUnion(u, sortBy); - const theTypes: Array = [] + const theTypes: Array = []; this.forEachUnionMember(u, nonNulls, "none", null, (_, t) => { theTypes.push(this.scalaType(t)); }); @@ -470,82 +477,265 @@ export class Scala3Renderer extends ConvenienceRenderer { } export class UpickleRenderer extends Scala3Renderer { + seenUnionTypes: Array = []; - protected emitClassDefinitionMethods() { - this.emitLine(") derives ReadWriter "); + protected upickleEncoderForType(t: Type, _ = false, noOptional = false, paramName: string = ""): Sourcelike { + return matchType( + t, + _anyType => ["OptionPickler.writeJs(", paramName, ")"], + _nullType => ["OptionPickler.writeJs(", paramName, ")"], + _boolType => ["OptionPickler.writeJs(", paramName, ")"], + _integerType => ["OptionPickler.writeJs(", paramName, ")"], + _doubleType => ["OptionPickler.writeJs(", paramName, ")"], + _stringType => ["OptionPickler.writeJs(", paramName, ")"], + arrayType => ["OptionPickler.writeJs[", this.scalaType(arrayType.items), "].apply(", paramName, ")"], + classType => ["OptionPickler.writeJs[", this.scalaType(classType), "].apply(", paramName, ")"], + mapType => ["OptionPickler.writeJs[String,", this.scalaType(mapType.values), "].apply(", paramName, ")"], + _ => ["OptionPickler.writeJs(", paramName, ")"], + unionType => { + const nullable = nullableFromUnion(unionType); + if (nullable !== null) { + if (noOptional) { + return ["OptionPickler.writeJs[", this.nameForNamedType(nullable), "]"]; + } else { + return ["OptionPickler.writeJs[Option[", this.nameForNamedType(nullable), "]]"]; + } + } + return ["OptionPickler.writeJs[", this.nameForNamedType(unionType), "]"]; + } + ); } - protected emitHeader(): void { - super.emitHeader(); - - this.emitLine("import upickle.default.*"); - this.ensureBlankLine(); + protected emitClassDefinitionMethods() { + this.emitLine(") derives OptionPickler.ReadWriter "); } -} - -export class Smithy4sRenderer extends Scala3Renderer { + protected anySourceType(optional: boolean): Sourcelike { + return [wrapOption("ujson.Value", optional)]; + } protected emitHeader(): void { - if (this.leadingComments !== undefined) { - this.emitCommentLines(this.leadingComments); - } else { - this.emitUsageHeader(); + super.emitHeader(); + const optionPickler = `object OptionPickler extends upickle.AttributeTagged : + import upickle.default.Writer + import upickle.default.Reader + override implicit def OptionWriter[T: Writer]: Writer[Option[T]] = + implicitly[Writer[T]].comap[Option[T]] { + case None => null.asInstanceOf[T] + case Some(x) => x } + override implicit def OptionReader[T: Reader]: Reader[Option[T]] = { + new Reader.Delegate[Any, Option[T]](implicitly[Reader[T]].map(Some(_))){ + override def visitNull(index: Int) = None + } + } +end OptionPickler + +object JsonExt: + val valueReader = OptionPickler.readwriter[ujson.Value] + def badMerge[T](r1: => OptionPickler.Reader[?], rest: OptionPickler.Reader[?]*): OptionPickler.Reader[T] = valueReader.map { json => + var t: T | Null = null + val stack = Vector.newBuilder[Throwable] + (r1 +: rest).foreach { reader => + if t == null then + try + t = OptionPickler.read[T](json, trace = true)(using reader.asInstanceOf[OptionPickler.Reader[T]]) + catch + case exc => stack += exc + } + if t != null then t.nn else throw new Exception(json.toString(), stack.result().headOption.getOrElse(null)) + } + + extension [T](r: OptionPickler.Reader[T]) def widen[K >: T] = r.map(_.asInstanceOf[K]) +end JsonExt +`; + // const singletonPickler = `given singletonStringPickler[A <: Singleton](using A <:< String): OptionPickler.ReadWriter[A] = OptionPickler.readwriter[ujson.Value].bimap[A]( + // _.toString(), + // str => { + // str.toString().asInstanceOf[A]() + // } + // )`; + this.emitMultiline(optionPickler); this.ensureBlankLine(); - this.emitLine("namespace ", this._scalaOptions.packageName); - this.ensureBlankLine(); + // this.emitMultiline(singletonPickler); } - protected emitTopLevelArray(t: ArrayType, name: Name): void { - const elementType = this.scalaType(t.items); - this.emitLine(["list ", name, " { member : ", elementType, "}"]); + protected override emitEmptyClassDefinition(c: ClassType, className: Name): void { + super.emitEmptyClassDefinition(c, className); + this.emitItem(" derives OptionPickler.ReadWriter"); + this.ensureBlankLine(); } - protected emitTopLevelMap(t: MapType, name: Name): void { - const elementType = this.scalaType(t.values); - this.emitLine(["map ", name, " { map[ key : String , value : ", elementType, "}"]); - } + protected override emitUnionDefinition(u: UnionType, unionName: Name): void { + function sortBy(t: Type): string { + const kind = t.kind; + if (kind === "class") return kind; + return "_" + kind; + } - protected emitEmptyClassDefinition(c: ClassType, className: Name): void { - this.emitDescription(this.descriptionForType(c)); - this.emitLine("structure ", className, "{}"); - } + this.emitDescription(this.descriptionForType(u)); + const [maybeNull, nonNulls] = removeNullFromUnion(u, false); + const theTypes: Array = []; + this.forEachUnionMember(u, nonNulls, "none", null, (_, t) => { + theTypes.push(this.scalaType(t)); + }); + if (maybeNull !== null) { + theTypes.push(this.nameForUnionMember(u, maybeNull)); + } + this.emitItem(["type ", unionName, " = "]); + theTypes.forEach((t, i) => { + this.emitItem(i === 0 ? t : [" | ", t]); + }); + const thisUnionType = theTypes.map(x => this.sourcelikeToString(x)).join(" | "); + + this.ensureBlankLine(); + if (!this.seenUnionTypes.some(y => y === thisUnionType)) { + this.seenUnionTypes.push(thisUnionType); + const sourceLikeTypes: Array<[Sourcelike, Type]> = []; + this.forEachUnionMember(u, nonNulls, "none", null, (_, t) => { + sourceLikeTypes.push([this.scalaType(t), t]); + }); + if (maybeNull !== null) { + sourceLikeTypes.push([this.nameForUnionMember(u, maybeNull), maybeNull]); + } + this.ensureBlankLine(); + this.emitLine([ + "given unionWriter", + unionName, + ": OptionPickler.Reader[", + unionName, + "] = JsonExt.badMerge[", + unionName, + "](" + ]); + this.indent(() => { + sourceLikeTypes.forEach(t => { + this.emitLine(["summon[OptionPickler.Reader[", t[0], "]],"]); + }); + this.emitLine(")"); + }); + this.ensureBlankLine(); + this.emitLine([ + "given unionReader", + unionName, + ": OptionPickler.Writer[", + unionName, + "] = OptionPickler.writer[ujson.Value].comap[", + unionName, + "]{ _v =>" + ]); + this.indent(() => { + this.emitLine("(_v: @unchecked) match "); + this.indent(() => { + sourceLikeTypes.forEach(t => { + this.emitLine(["case v: ", t[0], " => OptionPickler.write[", t[0], "](v)"]); + }); + }); + }); + this.emitLine("}"); + } + } protected emitEnumDefinition(e: EnumType, enumName: Name): void { this.emitDescription(this.descriptionForType(e)); - this.ensureBlankLine(); - this.emitItem(["enum ", enumName, " { "]); - let count = e.cases.size; - this.forEachEnumCase(e, "none", (name, jsonName) => { - // if (!(jsonName == "")) { - /* const backticks = - shouldAddBacktick(jsonName) || - jsonName.includes(" ") || - !isNaN(parseInt(jsonName.charAt(0))) - if (backticks) {this.emitItem("`")} else */ - this.emitLine(); - this.emitItem([name, " = \"", jsonName, "\""]); - // if (backticks) {this.emitItem("`")} - if (--count > 0) this.emitItem([","]); - //} else { - //--count - //} + let hasBlank = false; + this.forEachEnumCase(e, "none", (_, jsonName) => { + if (jsonName.trim() == "") { + hasBlank = true; + } }); this.ensureBlankLine(); - this.emitItem(["}"]); + if (hasBlank) { + //console.log("enumName: " + enumName + " has blank"); + this.emitItem(["type ", enumName, ' = "" | ', enumName, "NonBlank"]); + this.ensureBlankLine(); + this.emitLine([ + "given singleton", + enumName, + 'Pickler[A <: "" | ', + enumName, + "NonBlank]: OptionPickler.ReadWriter[A] = " + ]); - } + this.indent(() => { + this.emitLine(["OptionPickler.readwriter[ujson.Value].bimap[A]("]); + this.indent(() => { + this.emitLine(["_.toString(),"]); + this.emitLine(["str => {"]); + this.indent(() => { + this.emitLine(["val str2 = str.str"]); + this.emitLine(["str2 match {"]); + this.indent(() => { + this.emitLine(['case _ if str2.length == 0 => "".asInstanceOf[A] ']); + this.emitLine([ + "case parseable =>", + enumName, + "NonBlank.valueOf(parseable).asInstanceOf[A] " + ]); + }); + this.emitLine(["}"]); + }); + this.emitLine(["}"]); + }); + this.emitLine([")"]); + }); + this.ensureBlankLine(); + //let count = e.cases.size; + this.ensureBlankLine(); + this.emitLine(["enum ", enumName, "NonBlank derives OptionPickler.ReadWriter: "]); + this.indent(() => { + let count = e.cases.size; + this.forEachEnumCase(e, "none", (_, jsonName) => { + if (!(jsonName.trim() == "")) { + let strBuild = ""; + const backticks = + shouldAddBacktick(jsonName) || + jsonName.includes(" ") || + !isNaN(parseInt(jsonName.charAt(0))); + this.emitItem(["case "]); + if (backticks) { + strBuild = strBuild + "`"; + } + strBuild = strBuild + jsonName; + if (backticks) { + strBuild = strBuild + "`"; + } + if (--count > 0) strBuild + ","; + this.emitLine([strBuild]); + } + }); + }); + } else { + //console.log("enumName: " + enumName + " has non blank"); + this.emitLine(["enum ", enumName, " derives OptionPickler.ReadWriter: "]); + this.indent(() => { + let count = e.cases.size; + this.forEachEnumCase(e, "none", (_, jsonName) => { + let strBuild = ""; + const backticks = + shouldAddBacktick(jsonName) || jsonName.includes(" ") || !isNaN(parseInt(jsonName.charAt(0))); + this.emitItem(["case "]); + if (backticks) { + strBuild = strBuild + "`"; + } + strBuild = strBuild + jsonName; + if (backticks) { + strBuild = strBuild + "`"; + } + if (--count > 0) strBuild + ","; + this.emitLine([strBuild]); + }); + }); + } + } } - export class CirceRenderer extends Scala3Renderer { - seenUnionTypes: Array = []; protected circeEncoderForType(t: Type, _ = false, noOptional = false, paramName: string = ""): Sourcelike { @@ -602,15 +792,14 @@ export class CirceRenderer extends Scala3Renderer { jsonName.includes(" ") || !isNaN(parseInt(jsonName.charAt(0))) if (backticks) {this.emitItem("`")} else */ - this.emitItem(["\"", jsonName, "\""]); + this.emitItem(['"', jsonName, '"']); // if (backticks) {this.emitItem("`")} if (--count > 0) this.emitItem([" | "]); //} else { //--count - //} + //} }); this.ensureBlankLine(); - } protected emitHeader(): void { @@ -622,25 +811,45 @@ export class CirceRenderer extends Scala3Renderer { this.emitLine("import cats.syntax.functor._"); this.ensureBlankLine(); - this.emitLine("// For serialising string unions") - this.emitLine("given [A <: Singleton](using A <:< String): Decoder[A] = Decoder.decodeString.emapTry(x => Try(x.asInstanceOf[A])) "); - this.emitLine("given [A <: Singleton](using ev: A <:< String): Encoder[A] = Encoder.encodeString.contramap(ev) "); + this.emitLine("// For serialising string unions"); + this.emitLine( + "given [A <: Singleton](using A <:< String): Decoder[A] = Decoder.decodeString.emapTry(x => Try(x.asInstanceOf[A])) " + ); + this.emitLine( + "given [A <: Singleton](using ev: A <:< String): Encoder[A] = Encoder.encodeString.contramap(ev) " + ); this.ensureBlankLine(); - this.emitLine("// If a union has a null in, then we'll need this too... ") + this.emitLine("// If a union has a null in, then we'll need this too... "); this.emitLine("type NullValue = None.type"); } protected emitTopLevelArray(t: ArrayType, name: Name): void { super.emitTopLevelArray(t, name); const elementType = this.scalaType(t.items); - this.emitLine(["given (using ev : ", elementType, "): Encoder[Map[String,", elementType, "]] = Encoder.encodeMap[String, ", elementType, "]"]); + this.emitLine([ + "given (using ev : ", + elementType, + "): Encoder[Map[String,", + elementType, + "]] = Encoder.encodeMap[String, ", + elementType, + "]" + ]); } protected emitTopLevelMap(t: MapType, name: Name): void { super.emitTopLevelMap(t, name); const elementType = this.scalaType(t.values); this.ensureBlankLine(); - this.emitLine(["given (using ev : ", elementType, "): Encoder[Map[String, ", elementType, "]] = Encoder.encodeMap[String, ", elementType, "]"]); + this.emitLine([ + "given (using ev : ", + elementType, + "): Encoder[Map[String, ", + elementType, + "]] = Encoder.encodeMap[String, ", + elementType, + "]" + ]); } protected emitUnionDefinition(u: UnionType, unionName: Name): void { @@ -653,7 +862,7 @@ export class CirceRenderer extends Scala3Renderer { this.emitDescription(this.descriptionForType(u)); const [maybeNull, nonNulls] = removeNullFromUnion(u, sortBy); - const theTypes: Array = [] + const theTypes: Array = []; this.forEachUnionMember(u, nonNulls, "none", null, (_, t) => { theTypes.push(this.scalaType(t)); }); @@ -670,38 +879,43 @@ export class CirceRenderer extends Scala3Renderer { this.ensureBlankLine(); if (!this.seenUnionTypes.some(y => y === thisUnionType)) { this.seenUnionTypes.push(thisUnionType); - const sourceLikeTypes: Array<[Sourcelike, Type]> = [] + const sourceLikeTypes: Array<[Sourcelike, Type]> = []; this.forEachUnionMember(u, nonNulls, "none", null, (_, t) => { sourceLikeTypes.push([this.scalaType(t), t]); - }); if (maybeNull !== null) { sourceLikeTypes.push([this.nameForUnionMember(u, maybeNull), maybeNull]); } - this.emitLine(["given Decoder[", unionName, "] = {"]) + this.emitLine(["given Decoder[", unionName, "] = {"]); this.indent(() => { - this.emitLine(["List[Decoder[", unionName, "]]("]) + this.emitLine(["List[Decoder[", unionName, "]]("]); this.indent(() => { - sourceLikeTypes.forEach((t) => { + sourceLikeTypes.forEach(t => { this.emitLine(["Decoder[", t[0], "].widen,"]); }); - }) - this.emitLine(").reduceLeft(_ or _)") - } - ) - this.emitLine(["}"]) + }); + this.emitLine(").reduceLeft(_ or _)"); + }); + this.emitLine(["}"]); this.ensureBlankLine(); - this.emitLine(["given Encoder[", unionName, "] = Encoder.instance {"]) + this.emitLine(["given Encoder[", unionName, "] = Encoder.instance {"]); this.indent(() => { sourceLikeTypes.forEach((t, i) => { const paramTemp = `enc${i.toString()}`; - this.emitLine(["case ", paramTemp, " : ", t[0], " => ", this.circeEncoderForType(t[1], false, false, paramTemp)]); + this.emitLine([ + "case ", + paramTemp, + " : ", + t[0], + " => ", + this.circeEncoderForType(t[1], false, false, paramTemp) + ]); }); - }) - this.emitLine("}") + }); + this.emitLine("}"); } } } @@ -741,4 +955,3 @@ export class Scala3TargetLanguage extends TargetLanguage { } } } - diff --git a/test/fixtures/scala3/run.sh b/test/fixtures/scala3/run.sh index ff6223e387..7fa5377fbe 100755 --- a/test/fixtures/scala3/run.sh +++ b/test/fixtures/scala3/run.sh @@ -1,3 +1,3 @@ #!/usr/bin/env bash -scala-cli circe.scala TopLevel.scala +scala-cli --server=false circe.scala TopLevel.scala diff --git a/test/languages.ts b/test/languages.ts index 6027fbbcf5..a96091e49c 100644 --- a/test/languages.ts +++ b/test/languages.ts @@ -956,6 +956,91 @@ I havea no idea how to encode these tests correctly. sourceFiles: ["src/Language/Scala3.ts"], }; +export const Scala3UpickleLanguage: Language = { + name: "scala3", + base: "test/fixtures/scala3-upickle", + runCommand(sample: string) { + return `cp "${sample}" sample.json && ./run.sh`; + }, + diffViaSchema: true, + skipDiffViaSchema: [ + "bug427.json", + "keywords.json", + + ], + allowMissingNull: true, + features: ["enum", "union", "no-defaults"], + output: "TopLevel.scala", + topLevel: "TopLevel", + skipJSON: [ + // These tests have "_" as a param name. Scala can't do this? + "blns-object.json", + "identifiers.json", + "simple-identifiers.json", + "keywords.json", + + // these actually work as far as I can tell, but seem to fail because properties are sorted differently + // I don't think they fail... but I can't figure out sorting so hey ho let's skip them + "github-events.json", + "0a358.json", + "0a91a.json", + "34702.json", + "76ae1.json", + "af2d1.json", + "bug427.json", + "3d04a0.json", + + // Top level primitives... trivial, + // but annoying as it breaks compilation of the "Top Level" construct... which doesn't exist. + // It's too much hassle to fix + // and has no practical application in this context. Skip. + "no-classes.json", + + // spaces in variables names doesn't seem to work + "name-style.json", + +/* +I havea no idea how to encode these tests correctly. +*/ + "kitchen-sink.json", + "26c9c.json", + "421d4.json", + "a0496.json", + "fcca3.json", + "ae9ca.json", + "617e8.json", + "5f7fe.json", + "f74d5.json", + "a3d8c.json", + "combinations1.json", + "combinations2.json", + "combinations3.json", + "combinations4.json", + "unions.json", + "nst-test-suite.json", + + ], + skipSchema: [ + // 12 skips + "required.schema", + "multi-type-enum.schema", // I think it doesn't correctly realise this is an array of enums. + "integer-string.schema", + "intersection.schema", + "implicit-class-array-union.schema", + "date-time-or-string.schema", + "implicit-one-of.schema", + "go-schema-pattern-properties.schema", + "enum.schema", + "class-with-additional.schema", + "class-map-union.schema", + "keyword-unions.schema" + ], + skipMiscJSON: false, + rendererOptions: {framework: "upickle" }, + quickTestRendererOptions: [], + sourceFiles: ["src/Language/Scala3.ts"], +}; + export const Smithy4sLanguage: Language = { name: "smithy4a", base: "test/fixtures/smithy4s", From 879c82cf768a1b3f0f1a0c20205580172223dcc7 Mon Sep 17 00:00:00 2001 From: Simon Parten Date: Fri, 14 Apr 2023 13:36:22 +0200 Subject: [PATCH 02/31] Now with tests --- test/fixtures.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test/fixtures.ts b/test/fixtures.ts index 161dd44a42..cec9a772ea 100644 --- a/test/fixtures.ts +++ b/test/fixtures.ts @@ -809,8 +809,9 @@ export const allFixtures: Fixture[] = [ new JSONFixture(languages.FlowLanguage), new JSONFixture(languages.JavaScriptLanguage), new JSONFixture(languages.KotlinLanguage), - new JSONFixture(languages.Scala3Language), new JSONFixture(languages.KotlinJacksonLanguage, "kotlin-jackson"), + new JSONFixture(languages.Scala3Language), + new JSONFixture(languages.Scala3UpickleLanguage, "scala3-upickle"), new JSONFixture(languages.DartLanguage), new JSONFixture(languages.PikeLanguage), new JSONFixture(languages.HaskellLanguage), From 09b162001d83d952e38be746a1a8a23d1b070ff1 Mon Sep 17 00:00:00 2001 From: Simon Parten Date: Fri, 14 Apr 2023 13:37:40 +0200 Subject: [PATCH 03/31] That should test upickle --- test/fixtures/scala3-upickle/.gitignore | 4 ++++ test/fixtures/scala3-upickle/run.sh | 3 +++ test/fixtures/scala3-upickle/upickle.scala | 15 +++++++++++++++ test/fixtures/scala3/circe.scala | 6 +++--- 4 files changed, 25 insertions(+), 3 deletions(-) create mode 100644 test/fixtures/scala3-upickle/.gitignore create mode 100755 test/fixtures/scala3-upickle/run.sh create mode 100644 test/fixtures/scala3-upickle/upickle.scala diff --git a/test/fixtures/scala3-upickle/.gitignore b/test/fixtures/scala3-upickle/.gitignore new file mode 100644 index 0000000000..03df7b5603 --- /dev/null +++ b/test/fixtures/scala3-upickle/.gitignore @@ -0,0 +1,4 @@ +main.jar +.bsp +.scala-build +rename/ \ No newline at end of file diff --git a/test/fixtures/scala3-upickle/run.sh b/test/fixtures/scala3-upickle/run.sh new file mode 100755 index 0000000000..7707d38414 --- /dev/null +++ b/test/fixtures/scala3-upickle/run.sh @@ -0,0 +1,3 @@ +#!/usr/bin/env bash + +scala-cli upickle.scala TopLevel.scala diff --git a/test/fixtures/scala3-upickle/upickle.scala b/test/fixtures/scala3-upickle/upickle.scala new file mode 100644 index 0000000000..b3ce374c70 --- /dev/null +++ b/test/fixtures/scala3-upickle/upickle.scala @@ -0,0 +1,15 @@ +//> using scala "3.2.2" +//> using lib "com.lihaoyi::upickle:3.1.0" +//> using options "-Xmax-inlines", "500000" + +package quicktype +import upickle.default.* + + +@main def main = { + val json = scala.io.Source.fromFile("sample.json").getLines.mkString + val parsed = OptionPickler.read[TopLevel](json) + val jsonString = OptionPickler.writeJs(parsed) + val arr : Array[Byte] = jsonString.toString.getBytes("UTF-8") + System.out.write(arr, 0, arr.length) +} diff --git a/test/fixtures/scala3/circe.scala b/test/fixtures/scala3/circe.scala index b415f95451..169c22e8ea 100644 --- a/test/fixtures/scala3/circe.scala +++ b/test/fixtures/scala3/circe.scala @@ -1,6 +1,6 @@ -//> using scala "3.2.1" -//> using lib "io.circe::circe-core:0.15.0-M1" -//> using lib "io.circe::circe-parser:0.15.0-M1" +//> using scala "3.2.2" +//> using lib "io.circe::circe-core:0.14.5" +//> using lib "io.circe::circe-parser:0.14.5" //> using options "-Xmax-inlines", "3000" package quicktype From 7cc892f0712841b9a4c8dad49d0985892bd1eab7 Mon Sep 17 00:00:00 2001 From: Simon Parten Date: Fri, 14 Apr 2023 13:40:49 +0200 Subject: [PATCH 04/31] See if this runs the upickle suite --- .github/workflows/test-pr.yaml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/test-pr.yaml b/.github/workflows/test-pr.yaml index ed94095acf..33b34379a9 100644 --- a/.github/workflows/test-pr.yaml +++ b/.github/workflows/test-pr.yaml @@ -173,9 +173,13 @@ jobs: - name: Install scala if: ${{ contains(matrix.fixture, 'scala3') }} uses: VirtusLab/scala-cli-setup@main + - run: echo '@main def hello() = println("We need this spam print statement for bloop to exit correctly...")' | scala-cli _ if: ${{ contains(matrix.fixture, 'scala3') }} + - run: QUICKTEST=true FIXTURE=${{ matrix.fixture }} npm test + - run: echo '@main def hello() = println("We need this spam print statement for bloop to exit correctly...")' | scala-cli _ + if: ${{ contains(matrix.fixture, 'scala3-upickle') }} - run: QUICKTEST=true FIXTURE=${{ matrix.fixture }} npm test test-complete: From 3027c8d48823669158191e1ef1a0024cc4af1f8c Mon Sep 17 00:00:00 2001 From: Simon Parten Date: Fri, 14 Apr 2023 13:45:14 +0200 Subject: [PATCH 05/31] Remove extraneous declarations --- packages/quicktype-core/src/language/Scala3.ts | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/packages/quicktype-core/src/language/Scala3.ts b/packages/quicktype-core/src/language/Scala3.ts index 762912ab28..222f5849d1 100644 --- a/packages/quicktype-core/src/language/Scala3.ts +++ b/packages/quicktype-core/src/language/Scala3.ts @@ -19,7 +19,6 @@ import { TargetLanguage } from "../TargetLanguage"; import { ArrayType, ClassProperty, ClassType, EnumType, MapType, ObjectType, Type, UnionType } from "../Type"; import { matchType, nullableFromUnion, removeNullFromUnion } from "../TypeUtils"; import { RenderContext } from "../Renderer"; -import { json } from "stream/consumers"; export enum Framework { None, @@ -568,11 +567,11 @@ end JsonExt } protected override emitUnionDefinition(u: UnionType, unionName: Name): void { - function sortBy(t: Type): string { - const kind = t.kind; - if (kind === "class") return kind; - return "_" + kind; - } + // function sortBy(t: Type): string { + // const kind = t.kind; + // if (kind === "class") return kind; + // return "_" + kind; + // } this.emitDescription(this.descriptionForType(u)); From ecb282ff24eef4aa2cf18feed31a1f03ecf12e42 Mon Sep 17 00:00:00 2001 From: Simon Parten Date: Fri, 21 Apr 2023 11:36:38 +0200 Subject: [PATCH 06/31] Greatly improve enum encoding --- .github/workflows/test-pr.yaml | 4 - .../quicktype-core/src/language/Scala3.ts | 134 +++++-- test/fixtures.ts | 3 +- test/fixtures/scala3-upickle/upickle.scala | 2 + test/fixtures/scala3/circe.scala | 4 + test/fixtures/scala3/run.sh | 2 +- test/languages.ts | 363 +++++++----------- 7 files changed, 249 insertions(+), 263 deletions(-) diff --git a/.github/workflows/test-pr.yaml b/.github/workflows/test-pr.yaml index 33b34379a9..5e262d9eea 100644 --- a/.github/workflows/test-pr.yaml +++ b/.github/workflows/test-pr.yaml @@ -178,10 +178,6 @@ jobs: if: ${{ contains(matrix.fixture, 'scala3') }} - run: QUICKTEST=true FIXTURE=${{ matrix.fixture }} npm test - - run: echo '@main def hello() = println("We need this spam print statement for bloop to exit correctly...")' | scala-cli _ - if: ${{ contains(matrix.fixture, 'scala3-upickle') }} - - run: QUICKTEST=true FIXTURE=${{ matrix.fixture }} npm test - test-complete: needs: test runs-on: ubuntu-latest diff --git a/packages/quicktype-core/src/language/Scala3.ts b/packages/quicktype-core/src/language/Scala3.ts index 222f5849d1..80eddff855 100644 --- a/packages/quicktype-core/src/language/Scala3.ts +++ b/packages/quicktype-core/src/language/Scala3.ts @@ -351,6 +351,7 @@ export class Scala3Renderer extends ConvenienceRenderer { let count = c.getProperties().size; let first = true; this.forEachClassProperty(c, "none", (_, jsonName, p) => { + //console.log(jsonName); // Why is this in different order!!!!!! const nullable = p.type.kind === "union" && nullableFromUnion(p.type as UnionType) !== null; const nullableOrOptional = p.isOptional || p.type.kind === "null" || nullable; const last = --count === 0; @@ -395,7 +396,7 @@ export class Scala3Renderer extends ConvenienceRenderer { } protected emitEnumDefinition(e: EnumType, enumName: Name): void { - console.log("vanilla"); + //console.log("vanilla"); this.emitDescription(this.descriptionForType(e)); this.emitBlock( @@ -406,7 +407,7 @@ export class Scala3Renderer extends ConvenienceRenderer { this.emitItem("\t case "); } this.forEachEnumCase(e, "none", (name, jsonName) => { - console.log(jsonName); + //console.log(jsonName); if (!(jsonName == "")) { const backticks = shouldAddBacktick(jsonName) || @@ -749,7 +750,7 @@ export class CirceRenderer extends Scala3Renderer { arrayType => ["Encoder.encodeSeq[", this.scalaType(arrayType.items), "].apply(", paramName, ")"], classType => ["Encoder.AsObject[", this.scalaType(classType), "].apply(", paramName, ")"], mapType => ["Encoder.encodeMap[String,", this.scalaType(mapType.values), "].apply(", paramName, ")"], - _ => ["Encoder.encodeString(", paramName, ")"], + enumType => ["summon[Encoder[", this.scalaType(enumType), "]].apply(", paramName, ")"], unionType => { const nullable = nullableFromUnion(unionType); if (nullable !== null) { @@ -781,24 +782,105 @@ export class CirceRenderer extends Scala3Renderer { protected emitEnumDefinition(e: EnumType, enumName: Name): void { this.emitDescription(this.descriptionForType(e)); - this.ensureBlankLine(); - this.emitItem(["type ", enumName, " = "]); - let count = e.cases.size; + let hasBlank = false; this.forEachEnumCase(e, "none", (_, jsonName) => { - // if (!(jsonName == "")) { - /* const backticks = - shouldAddBacktick(jsonName) || - jsonName.includes(" ") || - !isNaN(parseInt(jsonName.charAt(0))) - if (backticks) {this.emitItem("`")} else */ - this.emitItem(['"', jsonName, '"']); - // if (backticks) {this.emitItem("`")} - if (--count > 0) this.emitItem([" | "]); - //} else { - //--count - //} + if (jsonName.trim() == "") { + hasBlank = true; + } }); this.ensureBlankLine(); + + const isBlank = (str: string): boolean => !str.trim(); + + if (hasBlank) { + //console.log("enumName: " + enumName + " has blank"); + this.emitItem(["type ", enumName, ' = "" | ', enumName, "NonBlank"]); + this.ensureBlankLine(); + this.emitLine([ + "given ", + enumName, + "Enc: Encoder[", + enumName, + "] = Encoder.encodeString.contramap(_.toString())" + ]); + this.emitLine(["given ", enumName, "Dec: Decoder[", enumName, "] = List[Decoder[", enumName, "]]("]); + this.indent(() => { + this.emitLine('Decoder[""].widen,'); + this.emitLine(["Decoder[", enumName, "NonBlank].widen"]); + }); + this.emitLine(").reduceLeft(_ or _)"); + + let count = e.cases.size - 1; + + this.ensureBlankLine(); + //let count = e.cases.size; + this.ensureBlankLine(); + this.emitLine(["enum ", enumName, "NonBlank :"]); + this.indent(() => { + let count = e.cases.size; + + this.forEachEnumCase(e, "none", (_, jsonName) => { + let strBuild = ""; + const backticks = + shouldAddBacktick(jsonName) || jsonName.includes(" ") || !isNaN(parseInt(jsonName.charAt(0))); + if (!isBlank(jsonName)) { + this.emitItem(["case "]); + } + if (backticks) { + strBuild = strBuild + "`"; + } + strBuild = strBuild + jsonName; + if (backticks) { + strBuild = strBuild + "`"; + } + if (--count > 0) strBuild + ","; + // don't emit the blank case + if (!isBlank(jsonName)) { + this.emitLine([strBuild]); + } + }); + }); + + this.emitLine([ + "given Decoder[", + enumName, + "NonBlank] = Decoder.decodeString.emapTry(x => Try(", + enumName, + "NonBlank.valueOf(x) ))" + ]); + this.emitLine("given Encoder[", enumName, "NonBlank] = Encoder.encodeString.contramap(_.toString())"); + } else { + //console.log("enumName: " + enumName + " has non blank"); + this.emitLine(["enum ", enumName, " : "]); + + this.indent(() => { + let count = e.cases.size; + this.forEachEnumCase(e, "none", (_, jsonName) => { + let strBuild = ""; + const backticks = + shouldAddBacktick(jsonName) || jsonName.includes(" ") || !isNaN(parseInt(jsonName.charAt(0))); + this.emitItem(["case "]); + if (backticks) { + strBuild = strBuild + "`"; + } + strBuild = strBuild + jsonName; + if (backticks) { + strBuild = strBuild + "`"; + } + if (--count > 0) strBuild + ","; + this.emitLine([strBuild]); + }); + }); + this.emitLine([ + "given Decoder[", + enumName, + "] = Decoder.decodeString.emapTry(x => Try(", + enumName, + ".valueOf(x) )) " + ]); + this.emitLine(["given Encoder[", enumName, "] = Encoder.encodeString.contramap(_.toString())"]); + this.ensureBlankLine(); + } } protected emitHeader(): void { @@ -811,12 +893,12 @@ export class CirceRenderer extends Scala3Renderer { this.ensureBlankLine(); this.emitLine("// For serialising string unions"); - this.emitLine( - "given [A <: Singleton](using A <:< String): Decoder[A] = Decoder.decodeString.emapTry(x => Try(x.asInstanceOf[A])) " - ); - this.emitLine( - "given [A <: Singleton](using ev: A <:< String): Encoder[A] = Encoder.encodeString.contramap(ev) " - ); + // this.emitLine( + // "given [A <: Singleton](using A <:< String): Decoder[A] = Decoder.decodeString.emapTry(x => Try(x.asInstanceOf[A])) " + // ); + // this.emitLine( + // "given [A <: Singleton](using ev: A <:< String): Encoder[A] = Encoder.encodeString.contramap(ev) " + // ); this.ensureBlankLine(); this.emitLine("// If a union has a null in, then we'll need this too... "); this.emitLine("type NullValue = None.type"); @@ -828,9 +910,9 @@ export class CirceRenderer extends Scala3Renderer { this.emitLine([ "given (using ev : ", elementType, - "): Encoder[Map[String,", + "): Encoder[Seq[", elementType, - "]] = Encoder.encodeMap[String, ", + "]] = Encoder.encodeSeq[", elementType, "]" ]); diff --git a/test/fixtures.ts b/test/fixtures.ts index cec9a772ea..d5932b4889 100644 --- a/test/fixtures.ts +++ b/test/fixtures.ts @@ -811,7 +811,7 @@ export const allFixtures: Fixture[] = [ new JSONFixture(languages.KotlinLanguage), new JSONFixture(languages.KotlinJacksonLanguage, "kotlin-jackson"), new JSONFixture(languages.Scala3Language), - new JSONFixture(languages.Scala3UpickleLanguage, "scala3-upickle"), + new JSONFixture(languages.Scala3UpickleLanguage, "scala3-upickle"), new JSONFixture(languages.DartLanguage), new JSONFixture(languages.PikeLanguage), new JSONFixture(languages.HaskellLanguage), @@ -836,6 +836,7 @@ export const allFixtures: Fixture[] = [ new JSONSchemaFixture(languages.KotlinLanguage), new JSONSchemaFixture(languages.KotlinJacksonLanguage, "schema-kotlin-jackson"), new JSONSchemaFixture(languages.Scala3Language), + new JSONSchemaFixture(languages.Scala3UpickleLanguage, "scala3-upickle"), new JSONSchemaFixture(languages.DartLanguage), new JSONSchemaFixture(languages.PikeLanguage), new JSONSchemaFixture(languages.HaskellLanguage), diff --git a/test/fixtures/scala3-upickle/upickle.scala b/test/fixtures/scala3-upickle/upickle.scala index b3ce374c70..5071588087 100644 --- a/test/fixtures/scala3-upickle/upickle.scala +++ b/test/fixtures/scala3-upickle/upickle.scala @@ -1,5 +1,6 @@ //> using scala "3.2.2" //> using lib "com.lihaoyi::upickle:3.1.0" +//> using lib "com.lihaoyi::os-lib:0.9.1" //> using options "-Xmax-inlines", "500000" package quicktype @@ -11,5 +12,6 @@ import upickle.default.* val parsed = OptionPickler.read[TopLevel](json) val jsonString = OptionPickler.writeJs(parsed) val arr : Array[Byte] = jsonString.toString.getBytes("UTF-8") + // os.write(os.pwd / "my.json", OptionPickler.write(parsed, 2)) System.out.write(arr, 0, arr.length) } diff --git a/test/fixtures/scala3/circe.scala b/test/fixtures/scala3/circe.scala index 169c22e8ea..318be045bc 100644 --- a/test/fixtures/scala3/circe.scala +++ b/test/fixtures/scala3/circe.scala @@ -7,6 +7,10 @@ package quicktype import io.circe._ import io.circe.parser._ import io.circe.syntax._ +import scala.util.Try +import scala.util.Right +import scala.util.Left + /* case class TopLevel ( diff --git a/test/fixtures/scala3/run.sh b/test/fixtures/scala3/run.sh index 7fa5377fbe..ff6223e387 100755 --- a/test/fixtures/scala3/run.sh +++ b/test/fixtures/scala3/run.sh @@ -1,3 +1,3 @@ #!/usr/bin/env bash -scala-cli --server=false circe.scala TopLevel.scala +scala-cli circe.scala TopLevel.scala diff --git a/test/languages.ts b/test/languages.ts index a96091e49c..3ee00849af 100644 --- a/test/languages.ts +++ b/test/languages.ts @@ -419,7 +419,7 @@ export const CJSONLanguage: Language = { "fcca3.json", "bug427.json", "github-events.json", - "keywords.json", + "keywords.json" ], allowMissingNull: false, features: ["minmax", "minmaxlength", "pattern", "enum", "union", "no-defaults"], @@ -439,7 +439,7 @@ export const CJSONLanguage: Language = { /* Map in Array in TopLevel is not supported (for the current implementation, can be added later, need recursivity) */ "combinations2.json", /* Array in Array in Union is not supported (for the current implementation, can be added later, need recursivity) */ - "combinations4.json", + "combinations4.json" ], skipMiscJSON: false, skipSchema: [ @@ -470,12 +470,10 @@ export const CJSONLanguage: Language = { "optional-any.schema", "required-non-properties.schema", /* Other cases not supported */ - "implicit-class-array-union.schema", + "implicit-class-array-union.schema" ], rendererOptions: {}, - quickTestRendererOptions: [ - { "source-style": "single-source" } - ], + quickTestRendererOptions: [{ "source-style": "single-source" }], sourceFiles: ["src/language/CJSON.ts"] }; @@ -872,246 +870,149 @@ export const FlowLanguage: Language = { }; export const Scala3Language: Language = { - name: "scala3", - base: "test/fixtures/scala3", - runCommand(sample: string) { - return `cp "${sample}" sample.json && ./run.sh`; - }, - diffViaSchema: true, - skipDiffViaSchema: [ - "bug427.json", - "keywords.json", - - ], - allowMissingNull: true, - features: ["enum", "union", "no-defaults"], - output: "TopLevel.scala", - topLevel: "TopLevel", - skipJSON: [ - // These tests have "_" as a param name. Scala can't do this? - "blns-object.json", - "identifiers.json", - "simple-identifiers.json", - "keywords.json", - - // these actually work as far as I can tell, but seem to fail because properties are sorted differently - // I don't think they fail... but I can't figure out sorting so hey ho let's skip them - "github-events.json", - "0a358.json", - "0a91a.json", - "34702.json", - "76ae1.json", - "af2d1.json", - "bug427.json", - "3d04a0.json", - - // Top level primitives... trivial, - // but annoying as it breaks compilation of the "Top Level" construct... which doesn't exist. - // It's too much hassle to fix - // and has no practical application in this context. Skip. - "no-classes.json", - - // spaces in variables names doesn't seem to work - "name-style.json", - -/* -I havea no idea how to encode these tests correctly. -*/ - "kitchen-sink.json", - "26c9c.json", - "421d4.json", - "a0496.json", - "fcca3.json", - "ae9ca.json", - "617e8.json", - "5f7fe.json", - "f74d5.json", - "a3d8c.json", - "combinations1.json", - "combinations2.json", - "combinations3.json", - "combinations4.json", - "unions.json", - "nst-test-suite.json", - - ], - skipSchema: [ - // 12 skips - "required.schema", - "multi-type-enum.schema", // I think it doesn't correctly realise this is an array of enums. - "integer-string.schema", - "intersection.schema", - "implicit-class-array-union.schema", - "date-time-or-string.schema", - "implicit-one-of.schema", - "go-schema-pattern-properties.schema", - "enum.schema", - "class-with-additional.schema", - "class-map-union.schema", - "keyword-unions.schema" - ], - skipMiscJSON: false, - rendererOptions: {framework: "circe" }, - quickTestRendererOptions: [], - sourceFiles: ["src/Language/Scala3.ts"], -}; - -export const Scala3UpickleLanguage: Language = { - name: "scala3", - base: "test/fixtures/scala3-upickle", - runCommand(sample: string) { - return `cp "${sample}" sample.json && ./run.sh`; - }, - diffViaSchema: true, - skipDiffViaSchema: [ - "bug427.json", - "keywords.json", + name: "scala3", + base: "test/fixtures/scala3", + runCommand(sample: string) { + return `cp "${sample}" sample.json && ./run.sh`; + }, + diffViaSchema: true, + skipDiffViaSchema: ["bug427.json", "keywords.json"], + allowMissingNull: true, + features: ["enum", "union", "no-defaults"], + output: "TopLevel.scala", + topLevel: "TopLevel", + skipJSON: [ + // These tests have "_" as a param name. Scala can't do this? + "blns-object.json", + "identifiers.json", + "simple-identifiers.json", + "keywords.json", - ], - allowMissingNull: true, - features: ["enum", "union", "no-defaults"], - output: "TopLevel.scala", - topLevel: "TopLevel", - skipJSON: [ - // These tests have "_" as a param name. Scala can't do this? - "blns-object.json", - "identifiers.json", - "simple-identifiers.json", - "keywords.json", + // these actually work as far as I can tell, but seem to fail because properties are sorted differently + // I don't think they fail... but I can't figure out sorting so hey ho let's skip them + "github-events.json", + "0a358.json", + "0a91a.json", + "34702.json", + "76ae1.json", + "af2d1.json", + "bug427.json", + "3d04a0.json", - // these actually work as far as I can tell, but seem to fail because properties are sorted differently - // I don't think they fail... but I can't figure out sorting so hey ho let's skip them - "github-events.json", - "0a358.json", - "0a91a.json", - "34702.json", - "76ae1.json", - "af2d1.json", - "bug427.json", - "3d04a0.json", + // Top level primitives... trivial, + // but annoying as it breaks compilation of the "Top Level" construct... which doesn't exist. + // It's too much hassle to fix + // and has no practical application in this context. Skip. + "no-classes.json", - // Top level primitives... trivial, - // but annoying as it breaks compilation of the "Top Level" construct... which doesn't exist. - // It's too much hassle to fix - // and has no practical application in this context. Skip. - "no-classes.json", - - // spaces in variables names doesn't seem to work - "name-style.json", + // spaces in variables names doesn't seem to work + "name-style.json", -/* + /* I havea no idea how to encode these tests correctly. */ - "kitchen-sink.json", - "26c9c.json", - "421d4.json", - "a0496.json", - "fcca3.json", - "ae9ca.json", - "617e8.json", - "5f7fe.json", - "f74d5.json", - "a3d8c.json", - "combinations1.json", - "combinations2.json", - "combinations3.json", - "combinations4.json", - "unions.json", - "nst-test-suite.json", - - ], - skipSchema: [ - // 12 skips - "required.schema", - "multi-type-enum.schema", // I think it doesn't correctly realise this is an array of enums. - "integer-string.schema", - "intersection.schema", - "implicit-class-array-union.schema", - "date-time-or-string.schema", - "implicit-one-of.schema", - "go-schema-pattern-properties.schema", - "enum.schema", - "class-with-additional.schema", - "class-map-union.schema", - "keyword-unions.schema" - ], - skipMiscJSON: false, - rendererOptions: {framework: "upickle" }, - quickTestRendererOptions: [], - sourceFiles: ["src/Language/Scala3.ts"], + "kitchen-sink.json", + "26c9c.json", + "421d4.json", + "a0496.json", + "fcca3.json", + "ae9ca.json", + "617e8.json", + "5f7fe.json", + "f74d5.json", + "a3d8c.json", + "combinations1.json", + "combinations2.json", + "combinations3.json", + "combinations4.json", + "unions.json", + "nst-test-suite.json" + ], + skipSchema: [ + // 12 skips + "required.schema", + "multi-type-enum.schema", // I think it doesn't correctly realise this is an array of enums. + "integer-string.schema", + "intersection.schema", + "implicit-class-array-union.schema", + "date-time-or-string.schema", + "implicit-one-of.schema", + "go-schema-pattern-properties.schema", + "enum.schema", + "class-with-additional.schema", + "class-map-union.schema", + "keyword-unions.schema" + ], + skipMiscJSON: false, + rendererOptions: { framework: "circe" }, + quickTestRendererOptions: [], + sourceFiles: ["src/Language/Scala3.ts"] }; export const Smithy4sLanguage: Language = { - name: "smithy4a", - base: "test/fixtures/smithy4s", - runCommand(sample: string) { - return `cp "${sample}" sample.json && ./run.sh`; - }, - diffViaSchema: true, - skipDiffViaSchema: [ - "bug427.json", - "keywords.json", + name: "smithy4a", + base: "test/fixtures/smithy4s", + runCommand(sample: string) { + return `cp "${sample}" sample.json && ./run.sh`; + }, + diffViaSchema: true, + skipDiffViaSchema: ["bug427.json", "keywords.json"], + allowMissingNull: true, + features: ["enum", "union", "no-defaults"], + output: "TopLevel.scala", + topLevel: "TopLevel", + skipJSON: [ + // These tests have "_" as a param name. Scala can't do this? + "blns-object.json", + "identifiers.json", + "simple-identifiers.json", + "keywords.json", - ], - allowMissingNull: true, - features: ["enum", "union", "no-defaults"], - output: "TopLevel.scala", - topLevel: "TopLevel", - skipJSON: [ - // These tests have "_" as a param name. Scala can't do this? - "blns-object.json", - "identifiers.json", - "simple-identifiers.json", - "keywords.json", + // these actually work as far as I can tell, but seem to fail because properties are sorted differently + // I don't think they fail... but I can't figure out sorting so hey ho let's skip them + "github-events.json", + "0a358.json", + "0a91a.json", + "34702.json", + "76ae1.json", + "af2d1.json", + "bug427.json", + "3d04a0.json", - // these actually work as far as I can tell, but seem to fail because properties are sorted differently - // I don't think they fail... but I can't figure out sorting so hey ho let's skip them - "github-events.json", - "0a358.json", - "0a91a.json", - "34702.json", - "76ae1.json", - "af2d1.json", - "bug427.json", - "3d04a0.json", + // Top level primitives... trivial, + // but annoying as it breaks compilation of the "Top Level" construct... which doesn't exist. + // It's too much hassle to fix + // and has no practical application in this context. Skip. + "no-classes.json", - // Top level primitives... trivial, - // but annoying as it breaks compilation of the "Top Level" construct... which doesn't exist. - // It's too much hassle to fix - // and has no practical application in this context. Skip. - "no-classes.json", - - // spaces in variables names doesn't seem to work - "name-style.json", + // spaces in variables names doesn't seem to work + "name-style.json", -/* + /* I havea no idea how to encode these tests correctly. */ - "kitchen-sink.json", - "26c9c.json", - "421d4.json", - "a0496.json", - "fcca3.json", - "ae9ca.json", - "617e8.json", - "5f7fe.json", - "f74d5.json", - "a3d8c.json", - "combinations1.json", - "combinations2.json", - "combinations3.json", - "combinations4.json", - "unions.json", - "nst-test-suite.json", - - ], - skipSchema: [ - - ], - skipMiscJSON: false, - rendererOptions: {framework: "just-types" }, - quickTestRendererOptions: [], - sourceFiles: ["src/Language/Smithy4s.ts"], + "kitchen-sink.json", + "26c9c.json", + "421d4.json", + "a0496.json", + "fcca3.json", + "ae9ca.json", + "617e8.json", + "5f7fe.json", + "f74d5.json", + "a3d8c.json", + "combinations1.json", + "combinations2.json", + "combinations3.json", + "combinations4.json", + "unions.json", + "nst-test-suite.json" + ], + skipSchema: [], + skipMiscJSON: false, + rendererOptions: { framework: "just-types" }, + quickTestRendererOptions: [], + sourceFiles: ["src/Language/Smithy4s.ts"] }; export const KotlinLanguage: Language = { From 5c23948418216682b21dc7e8c91937091172520f Mon Sep 17 00:00:00 2001 From: Simon Parten Date: Fri, 21 Apr 2023 11:51:26 +0200 Subject: [PATCH 07/31] Fix linting error --- packages/quicktype-core/src/language/Scala3.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/quicktype-core/src/language/Scala3.ts b/packages/quicktype-core/src/language/Scala3.ts index 80eddff855..c47166fd25 100644 --- a/packages/quicktype-core/src/language/Scala3.ts +++ b/packages/quicktype-core/src/language/Scala3.ts @@ -810,8 +810,6 @@ export class CirceRenderer extends Scala3Renderer { }); this.emitLine(").reduceLeft(_ or _)"); - let count = e.cases.size - 1; - this.ensureBlankLine(); //let count = e.cases.size; this.ensureBlankLine(); From c8afd56070a083a08e448cfbc94d119915f74e4a Mon Sep 17 00:00:00 2001 From: Simon Parten Date: Fri, 21 Apr 2023 11:54:59 +0200 Subject: [PATCH 08/31] Oops, remove non existing language --- test/fixtures.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/test/fixtures.ts b/test/fixtures.ts index d5932b4889..8d08527a85 100644 --- a/test/fixtures.ts +++ b/test/fixtures.ts @@ -811,7 +811,6 @@ export const allFixtures: Fixture[] = [ new JSONFixture(languages.KotlinLanguage), new JSONFixture(languages.KotlinJacksonLanguage, "kotlin-jackson"), new JSONFixture(languages.Scala3Language), - new JSONFixture(languages.Scala3UpickleLanguage, "scala3-upickle"), new JSONFixture(languages.DartLanguage), new JSONFixture(languages.PikeLanguage), new JSONFixture(languages.HaskellLanguage), @@ -836,7 +835,6 @@ export const allFixtures: Fixture[] = [ new JSONSchemaFixture(languages.KotlinLanguage), new JSONSchemaFixture(languages.KotlinJacksonLanguage, "schema-kotlin-jackson"), new JSONSchemaFixture(languages.Scala3Language), - new JSONSchemaFixture(languages.Scala3UpickleLanguage, "scala3-upickle"), new JSONSchemaFixture(languages.DartLanguage), new JSONSchemaFixture(languages.PikeLanguage), new JSONSchemaFixture(languages.HaskellLanguage), From 017c758bc1924732fc85887bb3af7ae6dd4d6e12 Mon Sep 17 00:00:00 2001 From: Simon Parten Date: Fri, 21 Apr 2023 12:09:29 +0200 Subject: [PATCH 09/31] Cant have wildcard enums --- test/languages.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test/languages.ts b/test/languages.ts index 3ee00849af..3d22568f6e 100644 --- a/test/languages.ts +++ b/test/languages.ts @@ -941,7 +941,8 @@ I havea no idea how to encode these tests correctly. "enum.schema", "class-with-additional.schema", "class-map-union.schema", - "keyword-unions.schema" + "keyword-unions.schema", + "keyword-enum.schema" ], skipMiscJSON: false, rendererOptions: { framework: "circe" }, From 77eeee2162e04f753df73765cc65820794381e10 Mon Sep 17 00:00:00 2001 From: Simon Parten Date: Wed, 17 May 2023 21:06:56 +0200 Subject: [PATCH 10/31] Try to fix some of the linting problems --- .../quicktype-core/src/language/Scala3.ts | 23 ++++++++++--------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/packages/quicktype-core/src/language/Scala3.ts b/packages/quicktype-core/src/language/Scala3.ts index c47166fd25..bf3ea237f7 100644 --- a/packages/quicktype-core/src/language/Scala3.ts +++ b/packages/quicktype-core/src/language/Scala3.ts @@ -1,5 +1,6 @@ import { anyTypeIssueAnnotation, nullTypeIssueAnnotation } from "../Annotation"; -import { ConvenienceRenderer, ForbiddenWordsInfo } from "../ConvenienceRenderer"; +import * as ConvenienceRenderer from "../ConvenienceRenderer"; +import { ForbiddenWordsInfo } from "../ConvenienceRenderer"; import { Name, Namer, funPrefixNamer } from "../Naming"; import { EnumOption, Option, StringOption, OptionValues, getOptionValues } from "../RendererOptions"; import { Sourcelike, maybeAnnotated } from "../Source"; @@ -206,7 +207,7 @@ export class Scala3Renderer extends ConvenienceRenderer { } protected forbiddenForEnumCases(_: EnumType, _enumName: Name): ForbiddenWordsInfo { - return { names: [], includeGlobalForbidden: true }; + return { names: ["_"], includeGlobalForbidden: true }; } protected forbiddenForUnionMembers(_u: UnionType, _unionName: Name): ForbiddenWordsInfo { @@ -408,7 +409,7 @@ export class Scala3Renderer extends ConvenienceRenderer { } this.forEachEnumCase(e, "none", (name, jsonName) => { //console.log(jsonName); - if (!(jsonName == "")) { + if (!(jsonName === "")) { const backticks = shouldAddBacktick(jsonName) || jsonName.includes(" ") || @@ -644,7 +645,7 @@ end JsonExt let hasBlank = false; this.forEachEnumCase(e, "none", (_, jsonName) => { - if (jsonName.trim() == "") { + if (jsonName.trim() === "") { hasBlank = true; } }); @@ -690,7 +691,7 @@ end JsonExt this.indent(() => { let count = e.cases.size; this.forEachEnumCase(e, "none", (_, jsonName) => { - if (!(jsonName.trim() == "")) { + if (!(jsonName.trim() === "")) { let strBuild = ""; const backticks = shouldAddBacktick(jsonName) || @@ -704,7 +705,7 @@ end JsonExt if (backticks) { strBuild = strBuild + "`"; } - if (--count > 0) strBuild + ","; + // if (--count > 0) strBuild + ","; this.emitLine([strBuild]); } }); @@ -727,7 +728,7 @@ end JsonExt if (backticks) { strBuild = strBuild + "`"; } - if (--count > 0) strBuild + ","; + // if (--count > 0) strBuild + ","; this.emitLine([strBuild]); }); }); @@ -784,7 +785,7 @@ export class CirceRenderer extends Scala3Renderer { let hasBlank = false; this.forEachEnumCase(e, "none", (_, jsonName) => { - if (jsonName.trim() == "") { + if (jsonName.trim() === "") { hasBlank = true; } }); @@ -815,7 +816,7 @@ export class CirceRenderer extends Scala3Renderer { this.ensureBlankLine(); this.emitLine(["enum ", enumName, "NonBlank :"]); this.indent(() => { - let count = e.cases.size; + //let count = e.cases.size; this.forEachEnumCase(e, "none", (_, jsonName) => { let strBuild = ""; @@ -831,7 +832,7 @@ export class CirceRenderer extends Scala3Renderer { if (backticks) { strBuild = strBuild + "`"; } - if (--count > 0) strBuild + ","; + //if (--count > 0) strBuild + ","; // don't emit the blank case if (!isBlank(jsonName)) { this.emitLine([strBuild]); @@ -865,7 +866,7 @@ export class CirceRenderer extends Scala3Renderer { if (backticks) { strBuild = strBuild + "`"; } - if (--count > 0) strBuild + ","; + // if (--count > 0) strBuild + ","; this.emitLine([strBuild]); }); }); From cfca1659a5abe07990f970436c0086aa4884d105 Mon Sep 17 00:00:00 2001 From: Simon Parten Date: Wed, 17 May 2023 21:24:47 +0200 Subject: [PATCH 11/31] Undo bad linting suggestions --- packages/quicktype-core/src/language/Scala3.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/quicktype-core/src/language/Scala3.ts b/packages/quicktype-core/src/language/Scala3.ts index bf3ea237f7..a0dafa7256 100644 --- a/packages/quicktype-core/src/language/Scala3.ts +++ b/packages/quicktype-core/src/language/Scala3.ts @@ -1,6 +1,5 @@ import { anyTypeIssueAnnotation, nullTypeIssueAnnotation } from "../Annotation"; -import * as ConvenienceRenderer from "../ConvenienceRenderer"; -import { ForbiddenWordsInfo } from "../ConvenienceRenderer"; +import { ConvenienceRenderer, ForbiddenWordsInfo } from "../ConvenienceRenderer"; import { Name, Namer, funPrefixNamer } from "../Naming"; import { EnumOption, Option, StringOption, OptionValues, getOptionValues } from "../RendererOptions"; import { Sourcelike, maybeAnnotated } from "../Source"; From 945899c3da6f69ec862eefd3215bc3b8d1265020 Mon Sep 17 00:00:00 2001 From: Ben DeLay Date: Wed, 26 Mar 2025 10:20:30 -0700 Subject: [PATCH 12/31] C#: Adds version 8 and expands nullability to reference types (#1632) --- .../quicktype-core/src/language/CSharp/CSharpRenderer.ts | 2 +- packages/quicktype-core/src/language/CSharp/language.ts | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/packages/quicktype-core/src/language/CSharp/CSharpRenderer.ts b/packages/quicktype-core/src/language/CSharp/CSharpRenderer.ts index 09cfcc3caf..d07dea90f8 100644 --- a/packages/quicktype-core/src/language/CSharp/CSharpRenderer.ts +++ b/packages/quicktype-core/src/language/CSharp/CSharpRenderer.ts @@ -125,7 +125,7 @@ export class CSharpRenderer extends ConvenienceRenderer { protected nullableCSType(t: Type, follow: (t: Type) => Type = followTargetType, withIssues = false): Sourcelike { t = followTargetType(t); const csType = this.csType(t, follow, withIssues); - if (isValueType(t)) { + if (isValueType(t) || this._csOptions.version >= 8) { return [csType, "?"]; } else { return csType; diff --git a/packages/quicktype-core/src/language/CSharp/language.ts b/packages/quicktype-core/src/language/CSharp/language.ts index e2b4605824..123147d4c5 100644 --- a/packages/quicktype-core/src/language/CSharp/language.ts +++ b/packages/quicktype-core/src/language/CSharp/language.ts @@ -16,7 +16,7 @@ export enum Framework { SystemTextJson = "SystemTextJson" } -export type Version = 5 | 6; +export type Version = 5 | 6 | 8; export interface OutputFeatures { attributes: boolean; helpers: boolean; @@ -55,7 +55,8 @@ export const cSharpOptions = { "C# version", [ ["5", 5], - ["6", 6] + ["6", 6], + ["8", 8] ], "6", "secondary" From 7fb43586d90a450bc454554bc8db7978552d20df Mon Sep 17 00:00:00 2001 From: Ben DeLay Date: Sun, 15 Jun 2025 19:46:57 -0700 Subject: [PATCH 13/31] C#: Adds JsonRequired attribute to System.Text.Json classes for required fields --- .../src/language/CSharp/SystemTextJsonCSharpRenderer.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/quicktype-core/src/language/CSharp/SystemTextJsonCSharpRenderer.ts b/packages/quicktype-core/src/language/CSharp/SystemTextJsonCSharpRenderer.ts index 77afed6da4..dfede9be4e 100644 --- a/packages/quicktype-core/src/language/CSharp/SystemTextJsonCSharpRenderer.ts +++ b/packages/quicktype-core/src/language/CSharp/SystemTextJsonCSharpRenderer.ts @@ -217,10 +217,13 @@ export class SystemTextJsonCSharpRenderer extends CSharpRenderer { const isNullable = followTargetType(property.type).isNullable; const isOptional = property.isOptional; - if (isOptional && !isNullable) { + if (!isOptional) { + attributes.push(["[", "JsonRequired", "]"]); + } else if (isOptional && !isNullable) { attributes.push(["[", "JsonIgnore", "(Condition = JsonIgnoreCondition.WhenWritingNull)]"]); } + // const requiredClass = this._options.dense ? "R" : "Required"; // const nullValueHandlingClass = this._options.dense ? "N" : "NullValueHandling"; // const nullValueHandling = isOptional && !isNullable ? [", NullValueHandling = ", nullValueHandlingClass, ".Ignore"] : []; From cbecc7f34c7d5131da1b7182431500e4fe7a168b Mon Sep 17 00:00:00 2001 From: Matthew Lee Date: Wed, 25 Jun 2025 22:26:34 -0500 Subject: [PATCH 14/31] Support for python 3.9 --- .../src/language/Python/JSONPythonRenderer.ts | 4 +-- .../src/language/Python/PythonRenderer.ts | 25 +++++++++++++++++-- .../src/language/Python/constants.ts | 2 ++ .../src/language/Python/language.ts | 7 +++--- 4 files changed, 31 insertions(+), 7 deletions(-) diff --git a/packages/quicktype-core/src/language/Python/JSONPythonRenderer.ts b/packages/quicktype-core/src/language/Python/JSONPythonRenderer.ts index b06965694f..1e96cca0f4 100644 --- a/packages/quicktype-core/src/language/Python/JSONPythonRenderer.ts +++ b/packages/quicktype-core/src/language/Python/JSONPythonRenderer.ts @@ -377,7 +377,7 @@ export class JSONPythonRenderer extends PythonRenderer { ", ", this.typingDecl("x", "Any"), ")", - this.typeHint(" -> ", this.withTyping("List"), "[", tvar, "]"), + this.typeHint(" -> ", this.pyOptions.features.builtinGenerics ? "list" : this.withTyping("List"), "[", tvar, "]"), ":", ], () => { @@ -618,7 +618,7 @@ export class JSONPythonRenderer extends PythonRenderer { (_integerType) => "int", (_doubleType) => "float", (_stringType) => "str", - (_arrayType) => "List", + (_arrayType) => this.pyOptions.features.builtinGenerics ? "list" : "List", (classType) => this.nameForNamedType(classType), (_mapType) => "dict", (enumType) => this.nameForNamedType(enumType), diff --git a/packages/quicktype-core/src/language/Python/PythonRenderer.ts b/packages/quicktype-core/src/language/Python/PythonRenderer.ts index fd7bb84e8a..80592d2f24 100644 --- a/packages/quicktype-core/src/language/Python/PythonRenderer.ts +++ b/packages/quicktype-core/src/language/Python/PythonRenderer.ts @@ -162,14 +162,14 @@ export class PythonRenderer extends ConvenienceRenderer { (_doubletype) => "float", (_stringType) => "str", (arrayType) => [ - this.withTyping("List"), + this.pyOptions.features.builtinGenerics ? "list" : this.withTyping("List"), "[", this.pythonType(arrayType.items), "]", ], (classType) => this.namedType(classType), (mapType) => [ - this.withTyping("Dict"), + this.pyOptions.features.builtinGenerics ? "dict" : this.withTyping("Dict"), "[str, ", this.pythonType(mapType.values), "]", @@ -196,6 +196,14 @@ export class PythonRenderer extends ConvenienceRenderer { } if (nonNulls.size > 1) { + if (this.pyOptions.features.builtinGenerics) { + return [ + arrayIntercalate(" | ", memberTypes), + " | None", + ...rest, + ]; + } + this.withImport("typing", "Union"); return [ this.withTyping("Optional"), @@ -206,6 +214,13 @@ export class PythonRenderer extends ConvenienceRenderer { ]; } + if (this.pyOptions.features.builtinGenerics) { + return [ + defined(iterableFirst(memberTypes)), + " | None", + ...rest, + ]; + } return [ this.withTyping("Optional"), "[", @@ -215,6 +230,12 @@ export class PythonRenderer extends ConvenienceRenderer { ]; } + if (this.pyOptions.features.builtinGenerics) { + return [ + arrayIntercalate(" | ", memberTypes), + ]; + } + return [ this.withTyping("Union"), "[", diff --git a/packages/quicktype-core/src/language/Python/constants.ts b/packages/quicktype-core/src/language/Python/constants.ts index d32c109235..1c96606977 100644 --- a/packages/quicktype-core/src/language/Python/constants.ts +++ b/packages/quicktype-core/src/language/Python/constants.ts @@ -5,7 +5,9 @@ export const forbiddenTypeNames = [ "None", "Enum", "List", + "list", "Dict", + "dict", "Optional", "Union", "Iterable", diff --git a/packages/quicktype-core/src/language/Python/language.ts b/packages/quicktype-core/src/language/Python/language.ts index c6a35b0dbf..524435e0a3 100644 --- a/packages/quicktype-core/src/language/Python/language.ts +++ b/packages/quicktype-core/src/language/Python/language.ts @@ -29,9 +29,10 @@ export const pythonOptions = { "python-version", "Python version", { - "3.5": { typeHints: false, dataClasses: false }, - "3.6": { typeHints: true, dataClasses: false }, - "3.7": { typeHints: true, dataClasses: true }, + "3.5": { typeHints: false, dataClasses: false, builtinGenerics: false }, + "3.6": { typeHints: true, dataClasses: false, builtinGenerics: false }, + "3.7": { typeHints: true, dataClasses: true, builtinGenerics: false }, + "3.9": { typeHints: true, dataClasses: true, builtinGenerics: true }, }, "3.6", ), From 2ad08bf4c4106e4b61cb3e7d377fd96b1894ba58 Mon Sep 17 00:00:00 2001 From: Matthew Lee Date: Thu, 26 Jun 2025 00:12:58 -0500 Subject: [PATCH 15/31] Fix bug --- .../quicktype-core/src/language/Python/JSONPythonRenderer.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/quicktype-core/src/language/Python/JSONPythonRenderer.ts b/packages/quicktype-core/src/language/Python/JSONPythonRenderer.ts index 1e96cca0f4..04be1538af 100644 --- a/packages/quicktype-core/src/language/Python/JSONPythonRenderer.ts +++ b/packages/quicktype-core/src/language/Python/JSONPythonRenderer.ts @@ -429,7 +429,7 @@ export class JSONPythonRenderer extends PythonRenderer { ")", this.typeHint( " -> ", - this.withTyping("Dict"), + this.pyOptions.features.builtinGenerics ? "dict" : "Dict", "[str, ", tvar, "]", @@ -620,7 +620,7 @@ export class JSONPythonRenderer extends PythonRenderer { (_stringType) => "str", (_arrayType) => this.pyOptions.features.builtinGenerics ? "list" : "List", (classType) => this.nameForNamedType(classType), - (_mapType) => "dict", + (_mapType) => this.pyOptions.features.builtinGenerics ? "dict" : "Dict", (enumType) => this.nameForNamedType(enumType), (_unionType) => undefined, (transformedStringType) => { From 6b00ecab06859bccd7b5e448e0a94c997f02cf68 Mon Sep 17 00:00:00 2001 From: Matthew Lee Date: Thu, 26 Jun 2025 00:17:07 -0500 Subject: [PATCH 16/31] Update Python version in tests to 3.9 --- .github/workflows/test-pr.yaml | 388 ++++++++++++++++----------------- test/fixtures/python/run.sh | 2 +- 2 files changed, 195 insertions(+), 195 deletions(-) diff --git a/.github/workflows/test-pr.yaml b/.github/workflows/test-pr.yaml index d497d9b1cc..5fc78c1118 100644 --- a/.github/workflows/test-pr.yaml +++ b/.github/workflows/test-pr.yaml @@ -1,197 +1,197 @@ name: Test PR on: - pull_request: - branches: - - master - - "release/**" + pull_request: + branches: + - master + - "release/**" jobs: - build: - runs-on: ubuntu-22.04 - steps: - - uses: actions/checkout@v3 - - uses: ./.github/workflows/setup - - test: - needs: [build] - runs-on: ${{ matrix.runs-on }} - - strategy: - fail-fast: true - - matrix: - fixture: - - typescript,typescript-zod,typescript-effect-schema - - javascript,schema-javascript - - golang,schema-golang - - cjson,schema-cjson - - cplusplus,schema-cplusplus - - flow,schema-flow - - java,schema-java - - python,schema-python - - haskell,schema-haskell - - csharp,schema-csharp,schema-json-csharp,graphql-csharp,csharp-SystemTextJson - - json-ts-csharp - - dart,schema-dart - # - swift,schema-swift # pgp issue - - javascript-prop-types - - ruby - - php - - scala3,schema-scala3 - - elixir,schema-elixir,graphql-elixir - - # Partially working - # - schema-typescript # TODO Unify with typescript once fixed - - # Implementation is too outdated to test in GitHub Actions - # - elm,schema-elm - - # Language is too niche / obscure to test easily on ubuntu-22.04 - # - pike,schema-pike - - # Not yet started - # @schani can you help me understand this fixture? - # It looks like it tests 13+ languages? - # - graphql - - # Never tested? - # - crystal - - runs-on: [ubuntu-22.04] - - include: - # Rust is very slow, so we use a larger runner - - fixture: rust,schema-rust - runs-on: ubuntu-latest-16-cores - # Kotlin is also slow - - fixture: kotlin,schema-kotlin,kotlin-jackson,schema-kotlin-jackson - runs-on: ubuntu-latest-16-cores - - # - fixture: objective-c # segfault on compiled test cmd - # runs-on: macos-latest - - name: ${{ matrix.fixture }} - steps: - - uses: actions/checkout@v3 - - uses: ./.github/workflows/setup - - - name: Setup PHP - if: ${{ contains(matrix.fixture, 'php') }} - uses: shivammathur/setup-php@v2 - with: - php-version: "8.2" - - - name: Setup Dart - if: ${{ contains(matrix.fixture, 'dart') }} - uses: dart-lang/setup-dart@v1.3 - with: - sdk: 2.14.4 - - - name: Setup Swift - if: ${{ contains(matrix.fixture, 'swift') }} - uses: swift-actions/setup-swift@v1 - with: - swift-version: "5.7.2" - - - name: Install Ruby - uses: ruby/setup-ruby@v1 - if: ${{ contains(matrix.fixture, 'ruby') }} - with: - ruby-version: "3.2.0" - - - name: Setup .NET Core SDK - if: ${{ contains(matrix.fixture, 'csharp') }} - uses: actions/setup-dotnet@v3 - with: - dotnet-version: 6 - - - name: Install Rust - if: ${{ contains(matrix.fixture, 'rust') }} - uses: actions-rs/toolchain@v1 - with: - toolchain: stable - - - name: Install Elm - if: ${{ contains(matrix.fixture, 'elm') }} - run: | - curl -L -o elm.gz https://github.com/elm/compiler/releases/download/0.19.1/binary-for-linux-64-bit.gz - gunzip elm.gz - chmod +x elm - sudo mv elm /usr/local/bin/ - - name: Install Haskell - if: ${{ contains(matrix.fixture, 'haskell') }} - run: | - if ! command -v stack > /dev/null 2>&1; then - curl -sL "https://get.haskellstack.org/" | sh - fi - - name: Install Python 3.7 - if: ${{ contains(matrix.fixture, 'python') }} - uses: actions/setup-python@v4 - with: - python-version: 3.7 - - - name: Install Python Dependencies - if: ${{ contains(matrix.fixture, 'python') }} - run: | - pip3.7 install mypy python-dateutil types-python-dateutil - - name: Install flow - if: ${{ contains(matrix.fixture, 'flow') }} - run: | - npm install -g flow-bin@0.66.0 flow-remove-types@1.2.3 - - name: Install Java - if: ${{ matrix.fixture == 'java,schema-java' || contains(matrix.fixture, 'kotlin') }} - uses: actions/setup-java@v3 - with: - java-version: "11" - distribution: "adopt" - - - name: Install Maven - if: ${{ matrix.fixture == 'java,schema-java' }} - uses: stCarolas/setup-maven@v4.5 - with: - maven-version: 3.8.2 - - - name: Install Kotlin - if: ${{ contains(matrix.fixture, 'kotlin') }} - uses: fwilhe2/setup-kotlin@main - - - name: Install go - if: ${{ contains(matrix.fixture, 'golang') }} - uses: actions/setup-go@v3 - with: - go-version: 1.15 - - - name: Install C - if: ${{ contains(matrix.fixture, 'cjson') }} - run: | - sudo apt-get update - sudo apt-get -y install build-essential valgrind - - - name: Install C++ - if: ${{ contains(matrix.fixture, 'cplusplus') }} - run: | - sudo apt-get update - sudo apt-get -y install libboost-all-dev software-properties-common g++ --assume-yes - - - name: Install scala - if: ${{ contains(matrix.fixture, 'scala3') }} - uses: VirtusLab/scala-cli-setup@main - - run: echo '@main def hello() = println("We need this spam print statement for bloop to exit correctly...")' | scala-cli _ - if: ${{ contains(matrix.fixture, 'scala3') }} - - - name: Install Elixir - if: ${{ contains(matrix.fixture, 'elixir') }} - uses: erlef/setup-beam@v1 - with: - elixir-version: "1.15.7" - otp-version: "26.0" - - - run: QUICKTEST=true FIXTURE=${{ matrix.fixture }} npm test - - test-complete: - if: ${{ cancelled() || contains(needs.*.result, 'cancelled') || contains(needs.*.result, 'failure') }} - needs: test - runs-on: ubuntu-22.04 - steps: - - run: | - echo "Some workflows have failed!" - exit 1 + build: + runs-on: ubuntu-22.04 + steps: + - uses: actions/checkout@v3 + - uses: ./.github/workflows/setup + + test: + needs: [build] + runs-on: ${{ matrix.runs-on }} + + strategy: + fail-fast: true + + matrix: + fixture: + - typescript,typescript-zod,typescript-effect-schema + - javascript,schema-javascript + - golang,schema-golang + - cjson,schema-cjson + - cplusplus,schema-cplusplus + - flow,schema-flow + - java,schema-java + - python,schema-python + - haskell,schema-haskell + - csharp,schema-csharp,schema-json-csharp,graphql-csharp,csharp-SystemTextJson + - json-ts-csharp + - dart,schema-dart + # - swift,schema-swift # pgp issue + - javascript-prop-types + - ruby + - php + - scala3,schema-scala3 + - elixir,schema-elixir,graphql-elixir + + # Partially working + # - schema-typescript # TODO Unify with typescript once fixed + + # Implementation is too outdated to test in GitHub Actions + # - elm,schema-elm + + # Language is too niche / obscure to test easily on ubuntu-22.04 + # - pike,schema-pike + + # Not yet started + # @schani can you help me understand this fixture? + # It looks like it tests 13+ languages? + # - graphql + + # Never tested? + # - crystal + + runs-on: [ubuntu-22.04] + + include: + # Rust is very slow, so we use a larger runner + - fixture: rust,schema-rust + runs-on: ubuntu-latest-16-cores + # Kotlin is also slow + - fixture: kotlin,schema-kotlin,kotlin-jackson,schema-kotlin-jackson + runs-on: ubuntu-latest-16-cores + + # - fixture: objective-c # segfault on compiled test cmd + # runs-on: macos-latest + + name: ${{ matrix.fixture }} + steps: + - uses: actions/checkout@v3 + - uses: ./.github/workflows/setup + + - name: Setup PHP + if: ${{ contains(matrix.fixture, 'php') }} + uses: shivammathur/setup-php@v2 + with: + php-version: "8.2" + + - name: Setup Dart + if: ${{ contains(matrix.fixture, 'dart') }} + uses: dart-lang/setup-dart@v1.3 + with: + sdk: 2.14.4 + + - name: Setup Swift + if: ${{ contains(matrix.fixture, 'swift') }} + uses: swift-actions/setup-swift@v1 + with: + swift-version: "5.7.2" + + - name: Install Ruby + uses: ruby/setup-ruby@v1 + if: ${{ contains(matrix.fixture, 'ruby') }} + with: + ruby-version: "3.2.0" + + - name: Setup .NET Core SDK + if: ${{ contains(matrix.fixture, 'csharp') }} + uses: actions/setup-dotnet@v3 + with: + dotnet-version: 6 + + - name: Install Rust + if: ${{ contains(matrix.fixture, 'rust') }} + uses: actions-rs/toolchain@v1 + with: + toolchain: stable + + - name: Install Elm + if: ${{ contains(matrix.fixture, 'elm') }} + run: | + curl -L -o elm.gz https://github.com/elm/compiler/releases/download/0.19.1/binary-for-linux-64-bit.gz + gunzip elm.gz + chmod +x elm + sudo mv elm /usr/local/bin/ + - name: Install Haskell + if: ${{ contains(matrix.fixture, 'haskell') }} + run: | + if ! command -v stack > /dev/null 2>&1; then + curl -sL "https://get.haskellstack.org/" | sh + fi + - name: Install Python 3.9 + if: ${{ contains(matrix.fixture, 'python') }} + uses: actions/setup-python@v4 + with: + python-version: 3.9 + + - name: Install Python Dependencies + if: ${{ contains(matrix.fixture, 'python') }} + run: | + pip3.9 install mypy python-dateutil types-python-dateutil + - name: Install flow + if: ${{ contains(matrix.fixture, 'flow') }} + run: | + npm install -g flow-bin@0.66.0 flow-remove-types@1.2.3 + - name: Install Java + if: ${{ matrix.fixture == 'java,schema-java' || contains(matrix.fixture, 'kotlin') }} + uses: actions/setup-java@v3 + with: + java-version: "11" + distribution: "adopt" + + - name: Install Maven + if: ${{ matrix.fixture == 'java,schema-java' }} + uses: stCarolas/setup-maven@v4.5 + with: + maven-version: 3.8.2 + + - name: Install Kotlin + if: ${{ contains(matrix.fixture, 'kotlin') }} + uses: fwilhe2/setup-kotlin@main + + - name: Install go + if: ${{ contains(matrix.fixture, 'golang') }} + uses: actions/setup-go@v3 + with: + go-version: 1.15 + + - name: Install C + if: ${{ contains(matrix.fixture, 'cjson') }} + run: | + sudo apt-get update + sudo apt-get -y install build-essential valgrind + + - name: Install C++ + if: ${{ contains(matrix.fixture, 'cplusplus') }} + run: | + sudo apt-get update + sudo apt-get -y install libboost-all-dev software-properties-common g++ --assume-yes + + - name: Install scala + if: ${{ contains(matrix.fixture, 'scala3') }} + uses: VirtusLab/scala-cli-setup@main + - run: echo '@main def hello() = println("We need this spam print statement for bloop to exit correctly...")' | scala-cli _ + if: ${{ contains(matrix.fixture, 'scala3') }} + + - name: Install Elixir + if: ${{ contains(matrix.fixture, 'elixir') }} + uses: erlef/setup-beam@v1 + with: + elixir-version: "1.15.7" + otp-version: "26.0" + + - run: QUICKTEST=true FIXTURE=${{ matrix.fixture }} npm test + + test-complete: + if: ${{ cancelled() || contains(needs.*.result, 'cancelled') || contains(needs.*.result, 'failure') }} + needs: test + runs-on: ubuntu-22.04 + steps: + - run: | + echo "Some workflows have failed!" + exit 1 diff --git a/test/fixtures/python/run.sh b/test/fixtures/python/run.sh index 0ad6e67872..98c4273a0b 100755 --- a/test/fixtures/python/run.sh +++ b/test/fixtures/python/run.sh @@ -3,7 +3,7 @@ if [ "x$QUICKTYPE_PYTHON_VERSION" = "x2.7" ] ; then PYTHON="python2.7" else - PYTHON="python3.7" + PYTHON="python3.9" fi "$PYTHON" "$@" From 9824c3e46aa0b96ca40b27ee8c5d4fc06fc0251d Mon Sep 17 00:00:00 2001 From: Matthew Lee Date: Thu, 26 Jun 2025 02:21:42 -0500 Subject: [PATCH 17/31] Fixed bug --- .../src/language/Python/PythonRenderer.ts | 25 ++++++++++++++----- 1 file changed, 19 insertions(+), 6 deletions(-) diff --git a/packages/quicktype-core/src/language/Python/PythonRenderer.ts b/packages/quicktype-core/src/language/Python/PythonRenderer.ts index 80592d2f24..f574a6dc13 100644 --- a/packages/quicktype-core/src/language/Python/PythonRenderer.ts +++ b/packages/quicktype-core/src/language/Python/PythonRenderer.ts @@ -144,13 +144,13 @@ export class PythonRenderer extends ConvenienceRenderer { return this.withImport("typing", name); } - protected namedType(t: Type): Sourcelike { + protected namedType(t: Type, _suppressQuotes: boolean = false): Sourcelike { const name = this.nameForNamedType(t); - if (this.declaredTypes.has(t)) return name; + if (_suppressQuotes || this.declaredTypes.has(t)) return name; return ["'", name, "'"]; } - protected pythonType(t: Type, _isRootTypeDef = false): Sourcelike { + protected pythonType(t: Type, _isRootTypeDef = false, _suppressQuotes = false): Sourcelike { const actualType = followTargetType(t); return matchType( @@ -167,20 +167,27 @@ export class PythonRenderer extends ConvenienceRenderer { this.pythonType(arrayType.items), "]", ], - (classType) => this.namedType(classType), + (classType) => this.namedType(classType, _suppressQuotes), (mapType) => [ this.pyOptions.features.builtinGenerics ? "dict" : this.withTyping("Dict"), "[str, ", this.pythonType(mapType.values), "]", ], - (enumType) => this.namedType(enumType), + (enumType) => this.namedType(enumType, _suppressQuotes), (unionType) => { const [hasNull, nonNulls] = removeNullFromUnion(unionType); + const hasClassOrEnumType = Array.from(nonNulls).some( + t => t instanceof ClassType || t instanceof EnumType + ); + const encapsulator = hasClassOrEnumType ? "'" : "" + const memberTypes = Array.from(nonNulls).map((m) => - this.pythonType(m), + // Suppress quotes for namedType + this.pythonType(m, false, true), ); + if (hasNull !== null) { const rest: string[] = []; if ( @@ -198,9 +205,11 @@ export class PythonRenderer extends ConvenienceRenderer { if (nonNulls.size > 1) { if (this.pyOptions.features.builtinGenerics) { return [ + encapsulator, arrayIntercalate(" | ", memberTypes), " | None", ...rest, + encapsulator ]; } @@ -216,9 +225,11 @@ export class PythonRenderer extends ConvenienceRenderer { if (this.pyOptions.features.builtinGenerics) { return [ + encapsulator, defined(iterableFirst(memberTypes)), " | None", ...rest, + encapsulator ]; } return [ @@ -232,7 +243,9 @@ export class PythonRenderer extends ConvenienceRenderer { if (this.pyOptions.features.builtinGenerics) { return [ + encapsulator, arrayIntercalate(" | ", memberTypes), + encapsulator ]; } From aba2303c959ef32b84631cb4ca8b8258ec596bc6 Mon Sep 17 00:00:00 2001 From: Matthew Lee Date: Thu, 26 Jun 2025 02:22:27 -0500 Subject: [PATCH 18/31] Use python 3.9 in tests --- packages/quicktype-core/src/language/Python/language.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/quicktype-core/src/language/Python/language.ts b/packages/quicktype-core/src/language/Python/language.ts index 524435e0a3..6bac0bb73f 100644 --- a/packages/quicktype-core/src/language/Python/language.ts +++ b/packages/quicktype-core/src/language/Python/language.ts @@ -34,7 +34,7 @@ export const pythonOptions = { "3.7": { typeHints: true, dataClasses: true, builtinGenerics: false }, "3.9": { typeHints: true, dataClasses: true, builtinGenerics: true }, }, - "3.6", + "3.9", ), justTypes: new BooleanOption("just-types", "Classes only", false), nicePropertyNames: new BooleanOption( From bf6b30751228e8f328f0dab08d475ef1ecc86386 Mon Sep 17 00:00:00 2001 From: k-vasily Date: Wed, 11 Mar 2026 16:48:49 +0000 Subject: [PATCH 19/31] feat: add --prefer-unknown option for TypeScript and Flow Add a `--prefer-unknown` CLI flag that emits `unknown` instead of `any` in TypeScript output, and `mixed` instead of `any` in Flow output. This is a fresh implementation of the feature proposed in PR #2194 by @RichiCoder1, adapted to the current codebase structure where TypeScriptFlow was split into multiple files. Closes #1619 --- .../language/TypeScriptFlow/FlowRenderer.ts | 15 +++++++++++++-- .../TypeScriptFlowBaseRenderer.ts | 13 ++++++++----- .../TypeScriptFlow/TypeScriptRenderer.ts | 19 +++++++++++++++---- .../src/language/TypeScriptFlow/language.ts | 5 +++++ test/languages.ts | 1 + 5 files changed, 42 insertions(+), 11 deletions(-) diff --git a/packages/quicktype-core/src/language/TypeScriptFlow/FlowRenderer.ts b/packages/quicktype-core/src/language/TypeScriptFlow/FlowRenderer.ts index 02e82aee57..99ead7d74f 100644 --- a/packages/quicktype-core/src/language/TypeScriptFlow/FlowRenderer.ts +++ b/packages/quicktype-core/src/language/TypeScriptFlow/FlowRenderer.ts @@ -5,15 +5,26 @@ import type { ClassType, EnumType } from "../../Type"; import type { JavaScriptTypeAnnotations } from "../JavaScript"; import { TypeScriptFlowBaseRenderer } from "./TypeScriptFlowBaseRenderer"; -import { tsFlowTypeAnnotations } from "./utils"; export class FlowRenderer extends TypeScriptFlowBaseRenderer { + protected anyType(): string { + return this._tsFlowOptions.preferUnknown ? "mixed" : "any"; + } + protected forbiddenNamesForGlobalNamespace(): string[] { return ["Class", "Date", "Object", "String", "Array", "JSON", "Error"]; } protected get typeAnnotations(): JavaScriptTypeAnnotations { - return Object.assign({ never: "" }, tsFlowTypeAnnotations); + const a = this.anyType(); + return Object.assign({ never: "" }, { + any: `: ${a}`, + anyArray: `: ${a}[]`, + anyMap: `: { [k: string]: ${a} }`, + string: ": string", + stringArray: ": string[]", + boolean: ": boolean", + }); } protected emitEnum(e: EnumType, enumName: Name): void { diff --git a/packages/quicktype-core/src/language/TypeScriptFlow/TypeScriptFlowBaseRenderer.ts b/packages/quicktype-core/src/language/TypeScriptFlow/TypeScriptFlowBaseRenderer.ts index 2acc4da237..95d6dd8ec2 100644 --- a/packages/quicktype-core/src/language/TypeScriptFlow/TypeScriptFlowBaseRenderer.ts +++ b/packages/quicktype-core/src/language/TypeScriptFlow/TypeScriptFlowBaseRenderer.ts @@ -64,7 +64,7 @@ export abstract class TypeScriptFlowBaseRenderer extends JavaScriptRenderer { return matchType( t, - (_anyType) => singleWord("any"), + (_anyType) => singleWord(this.anyType()), (_nullType) => singleWord("null"), (_boolType) => singleWord("boolean"), (_integerType) => singleWord("number"), @@ -113,6 +113,8 @@ export abstract class TypeScriptFlowBaseRenderer extends JavaScriptRenderer { ); } + protected abstract anyType(): string; + protected abstract emitEnum(e: EnumType, enumName: Name): void; protected abstract emitClassBlock(c: ClassType, className: Name): void; @@ -198,7 +200,7 @@ export abstract class TypeScriptFlowBaseRenderer extends JavaScriptRenderer { protected deserializerFunctionLine(t: Type, name: Name): Sourcelike { const jsonType = - this._tsFlowOptions.rawType === "json" ? "string" : "any"; + this._tsFlowOptions.rawType === "json" ? "string" : this.anyType(); return [ "function to", name, @@ -212,7 +214,7 @@ export abstract class TypeScriptFlowBaseRenderer extends JavaScriptRenderer { protected serializerFunctionLine(t: Type, name: Name): Sourcelike { const camelCaseName = modifySource(camelCase, name); const returnType = - this._tsFlowOptions.rawType === "json" ? "string" : "any"; + this._tsFlowOptions.rawType === "json" ? "string" : this.anyType(); return [ "function ", camelCaseName, @@ -228,9 +230,10 @@ export abstract class TypeScriptFlowBaseRenderer extends JavaScriptRenderer { } protected get castFunctionLines(): [string, string] { + const a = this.anyType(); return [ - "function cast(val: any, typ: any): T", - "function uncast(val: T, typ: any): any", + `function cast(val: ${a}, typ: ${a}): T`, + `function uncast(val: T, typ: ${a}): ${a}`, ]; } diff --git a/packages/quicktype-core/src/language/TypeScriptFlow/TypeScriptRenderer.ts b/packages/quicktype-core/src/language/TypeScriptFlow/TypeScriptRenderer.ts index 82d272484c..ceb00a668f 100644 --- a/packages/quicktype-core/src/language/TypeScriptFlow/TypeScriptRenderer.ts +++ b/packages/quicktype-core/src/language/TypeScriptFlow/TypeScriptRenderer.ts @@ -6,16 +6,19 @@ import { isNamedType } from "../../Type/TypeUtils"; import type { JavaScriptTypeAnnotations } from "../JavaScript"; import { TypeScriptFlowBaseRenderer } from "./TypeScriptFlowBaseRenderer"; -import { tsFlowTypeAnnotations } from "./utils"; export class TypeScriptRenderer extends TypeScriptFlowBaseRenderer { + protected anyType(): string { + return this._tsFlowOptions.preferUnknown ? "unknown" : "any"; + } + protected forbiddenNamesForGlobalNamespace(): string[] { return ["Array", "Date"]; } protected deserializerFunctionLine(t: Type, name: Name): Sourcelike { const jsonType = - this._tsFlowOptions.rawType === "json" ? "string" : "any"; + this._tsFlowOptions.rawType === "json" ? "string" : this.anyType(); return [ "public static to", name, @@ -29,7 +32,7 @@ export class TypeScriptRenderer extends TypeScriptFlowBaseRenderer { protected serializerFunctionLine(t: Type, name: Name): Sourcelike { const camelCaseName = modifySource(camelCase, name); const returnType = - this._tsFlowOptions.rawType === "json" ? "string" : "any"; + this._tsFlowOptions.rawType === "json" ? "string" : this.anyType(); return [ "public static ", camelCaseName, @@ -45,7 +48,15 @@ export class TypeScriptRenderer extends TypeScriptFlowBaseRenderer { } protected get typeAnnotations(): JavaScriptTypeAnnotations { - return Object.assign({ never: ": never" }, tsFlowTypeAnnotations); + const a = this.anyType(); + return Object.assign({ never: ": never" }, { + any: `: ${a}`, + anyArray: `: ${a}[]`, + anyMap: `: { [k: string]: ${a} }`, + string: ": string", + stringArray: ": string[]", + boolean: ": boolean", + }); } protected emitModuleExports(): void { diff --git a/packages/quicktype-core/src/language/TypeScriptFlow/language.ts b/packages/quicktype-core/src/language/TypeScriptFlow/language.ts index 1a5bdde8d0..a0427f66f6 100644 --- a/packages/quicktype-core/src/language/TypeScriptFlow/language.ts +++ b/packages/quicktype-core/src/language/TypeScriptFlow/language.ts @@ -40,6 +40,11 @@ export const tsFlowOptions = Object.assign({}, javaScriptOptions, { false, ), readonly: new BooleanOption("readonly", "Use readonly type members", false), + preferUnknown: new BooleanOption( + "prefer-unknown", + "Use unknown instead of any type", + false, + ), }); export const typeScriptLanguageConfig = { diff --git a/test/languages.ts b/test/languages.ts index 9573f3e574..f4f564de4d 100644 --- a/test/languages.ts +++ b/test/languages.ts @@ -839,6 +839,7 @@ export const TypeScriptLanguage: Language = { { "acronym-style": "pascal" }, { converters: "all-objects" }, { readonly: "true" }, + { "prefer-unknown": "true" }, ], sourceFiles: ["src/language/TypeScript/index.ts"], }; From 5cd5112bbc41e6ea28085ebf77a8db52dd177053 Mon Sep 17 00:00:00 2001 From: Mark Probst Date: Sun, 19 Jul 2026 18:31:32 -0400 Subject: [PATCH 20/31] C#: fix up version-8 nullable-reference-types support Fixes on top of PR #2694: - Gate the System.Text.Json [JsonRequired] attribute behind the existing check-required option instead of emitting it unconditionally. It now matches the spirit of Newtonsoft's [JsonProperty(Required = ...)]: only emitted for non-optional properties when check-required is on. - Un-skip required.schema, strict-optional.schema and intersection.schema for the System.Text.Json fixture: with [JsonRequired] the renderer now implements check-required, so the expected-failure samples fail as expected. - Emit "#nullable enable" (plus the same CS8618/CS8601/CS8603 pragma suppressions the System.Text.Json renderer already uses) in the Newtonsoft renderer when csharp-version >= 8, so the new "?" reference type annotations compile without warnings. Versions 5 and 6 are unchanged. - Bump the C# test fixtures to net8.0 (and CI to the .NET 8 SDK): [JsonRequired] requires System.Text.Json 7+. - Add { "csharp-version": "8" } quick-test combinations for both the Newtonsoft and System.Text.Json fixtures. Co-Authored-By: Claude Fable 5 --- .github/workflows/test-pr.yaml | 2 +- .../language/CSharp/NewtonSoftCSharpRenderer.ts | 15 +++++++++++++++ .../CSharp/SystemTextJsonCSharpRenderer.ts | 11 ++++++++--- test/fixtures/csharp-SystemTextJson/test.csproj | 4 ++-- test/fixtures/csharp/test.csproj | 2 +- test/languages.ts | 5 ++--- 6 files changed, 29 insertions(+), 10 deletions(-) diff --git a/.github/workflows/test-pr.yaml b/.github/workflows/test-pr.yaml index 6f67d78118..de709a7234 100644 --- a/.github/workflows/test-pr.yaml +++ b/.github/workflows/test-pr.yaml @@ -106,7 +106,7 @@ jobs: if: ${{ contains(matrix.fixture, 'csharp') }} uses: actions/setup-dotnet@v3 with: - dotnet-version: 6 + dotnet-version: 8 - name: Install Rust if: ${{ contains(matrix.fixture, 'rust') }} diff --git a/packages/quicktype-core/src/language/CSharp/NewtonSoftCSharpRenderer.ts b/packages/quicktype-core/src/language/CSharp/NewtonSoftCSharpRenderer.ts index 3c3d91cc9c..207dda662c 100644 --- a/packages/quicktype-core/src/language/CSharp/NewtonSoftCSharpRenderer.ts +++ b/packages/quicktype-core/src/language/CSharp/NewtonSoftCSharpRenderer.ts @@ -190,6 +190,14 @@ export class NewtonsoftCSharpRenderer extends CSharpRenderer { return this._options.baseclass; } + protected emitDefaultFollowingComments(): void { + if (!this._needHelpers || this._options.version < 8) return; + + this.emitLine("#pragma warning restore CS8618"); + this.emitLine("#pragma warning restore CS8601"); + this.emitLine("#pragma warning restore CS8603"); + } + protected emitDefaultLeadingComments(): void { if (!this._needHelpers) return; @@ -223,6 +231,13 @@ export class NewtonsoftCSharpRenderer extends CSharpRenderer { ";", ); }); + + if (this._options.version >= 8) { + this.emitLine("#nullable enable"); + this.emitLine("#pragma warning disable CS8618"); + this.emitLine("#pragma warning disable CS8601"); + this.emitLine("#pragma warning disable CS8603"); + } } private converterForType(t: Type): Name | undefined { diff --git a/packages/quicktype-core/src/language/CSharp/SystemTextJsonCSharpRenderer.ts b/packages/quicktype-core/src/language/CSharp/SystemTextJsonCSharpRenderer.ts index 08a4de9e8b..4acfb7e6e7 100644 --- a/packages/quicktype-core/src/language/CSharp/SystemTextJsonCSharpRenderer.ts +++ b/packages/quicktype-core/src/language/CSharp/SystemTextJsonCSharpRenderer.ts @@ -273,9 +273,14 @@ export class SystemTextJsonCSharpRenderer extends CSharpRenderer { const isNullable = followTargetType(property.type).isNullable; const isOptional = property.isOptional; - if (!isOptional) { - attributes.push(["[", "JsonRequired", "]"]); - } else if (isOptional && !isNullable) { + // [JsonRequired] makes deserialization fail if the property is + // missing from the JSON. It requires System.Text.Json 7.0 or + // later (.NET 7+). + if (this._options.checkRequired && !isOptional) { + attributes.push(["[JsonRequired]"]); + } + + if (isOptional && !isNullable) { attributes.push([ "[", "JsonIgnore", diff --git a/test/fixtures/csharp-SystemTextJson/test.csproj b/test/fixtures/csharp-SystemTextJson/test.csproj index 1f5224ffd3..37d7e4c935 100755 --- a/test/fixtures/csharp-SystemTextJson/test.csproj +++ b/test/fixtures/csharp-SystemTextJson/test.csproj @@ -1,9 +1,9 @@  Exe - net6 + net8.0 - + diff --git a/test/fixtures/csharp/test.csproj b/test/fixtures/csharp/test.csproj index 2fbf1477c5..9b3c4614b1 100755 --- a/test/fixtures/csharp/test.csproj +++ b/test/fixtures/csharp/test.csproj @@ -1,7 +1,7 @@  Exe - netcoreapp6.0 + net8.0 diff --git a/test/languages.ts b/test/languages.ts index 638c3a91d2..4ad55268cc 100644 --- a/test/languages.ts +++ b/test/languages.ts @@ -118,6 +118,7 @@ export const CSharpLanguage: Language = { quickTestRendererOptions: [ { "array-type": "list" }, { "csharp-version": "5" }, + { "csharp-version": "8" }, { density: "dense" }, { "number-type": "decimal" }, { "any-type": "dynamic" }, @@ -160,14 +161,12 @@ export const CSharpLanguageSystemTextJson: Language = { "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 - "required.schema", // the renderer doesn't implement check-required, so the expected-failure sample doesn't fail - "strict-optional.schema", // the renderer doesn't implement check-required, so the expected-failure sample doesn't fail - "intersection.schema", // the renderer doesn't implement check-required, so the expected-failure sample doesn't fail ], rendererOptions: { "check-required": "true", framework: "SystemTextJson" }, quickTestRendererOptions: [ { "array-type": "list" }, { "csharp-version": "6" }, + { "csharp-version": "8" }, { density: "dense" }, { "number-type": "decimal" }, { "any-type": "dynamic" }, From 325cb37123a2d94d09b1a42407848e710b174641 Mon Sep 17 00:00:00 2001 From: Mark Probst Date: Sun, 19 Jul 2026 18:33:04 -0400 Subject: [PATCH 21/31] Fix Python 3.10 type-hint syntax support - Rename the new python-version level to 3.10: PEP 604 unions (X | None) require Python 3.10, not 3.9. Split the feature flag into builtinGenerics (PEP 585, 3.9+) and unionOperators (PEP 604, 3.10+), which also yields a 3.9 level with builtin generics but typing Optional/Union. Make 3.10 the default python-version. - Emit " = None" defaults outside the quoted annotation: the annotation 'Foo | None = None' is invalid syntax. - Only quote PEP 604 unions when a member actually needs a forward reference, and never quote inside an already-quoted annotation. - Restore withTyping("Dict") registration in the dict converter so the typing import is emitted for pre-3.9 versions, and revert typeObject's map branch to the runtime type "dict". - Cover the legacy typing.* code paths in QUICKTEST via quickTestRendererOptions for 3.5/3.6/3.7/3.9. - Run python fixtures under Python 3.12 in CI and the fixture driver. Co-Authored-By: Claude Fable 5 --- .github/workflows/test-pr.yaml | 6 +- .../src/language/Python/JSONPythonRenderer.ts | 19 ++- .../src/language/Python/PythonRenderer.ts | 145 ++++++++++++------ .../src/language/Python/language.ts | 42 ++++- test/fixtures/python/run.sh | 2 +- test/languages.ts | 8 +- 6 files changed, 163 insertions(+), 59 deletions(-) diff --git a/.github/workflows/test-pr.yaml b/.github/workflows/test-pr.yaml index f7eec829a7..984b65a42a 100644 --- a/.github/workflows/test-pr.yaml +++ b/.github/workflows/test-pr.yaml @@ -127,16 +127,16 @@ jobs: if ! command -v stack > /dev/null 2>&1; then curl -sL "https://get.haskellstack.org/" | sh fi - - name: Install Python 3.9 + - name: Install Python 3.12 if: ${{ contains(matrix.fixture, 'python') }} uses: actions/setup-python@v4 with: - python-version: 3.9 + python-version: "3.12" - name: Install Python Dependencies if: ${{ contains(matrix.fixture, 'python') }} run: | - pip3.9 install mypy python-dateutil types-python-dateutil + pip3.12 install mypy python-dateutil types-python-dateutil - name: Install flow if: ${{ contains(matrix.fixture, 'flow') }} run: | diff --git a/packages/quicktype-core/src/language/Python/JSONPythonRenderer.ts b/packages/quicktype-core/src/language/Python/JSONPythonRenderer.ts index 04be1538af..9a18427426 100644 --- a/packages/quicktype-core/src/language/Python/JSONPythonRenderer.ts +++ b/packages/quicktype-core/src/language/Python/JSONPythonRenderer.ts @@ -377,7 +377,15 @@ export class JSONPythonRenderer extends PythonRenderer { ", ", this.typingDecl("x", "Any"), ")", - this.typeHint(" -> ", this.pyOptions.features.builtinGenerics ? "list" : this.withTyping("List"), "[", tvar, "]"), + this.typeHint( + " -> ", + this.pyOptions.features.builtinGenerics + ? "list" + : this.withTyping("List"), + "[", + tvar, + "]", + ), ":", ], () => { @@ -429,7 +437,9 @@ export class JSONPythonRenderer extends PythonRenderer { ")", this.typeHint( " -> ", - this.pyOptions.features.builtinGenerics ? "dict" : "Dict", + this.pyOptions.features.builtinGenerics + ? "dict" + : this.withTyping("Dict"), "[str, ", tvar, "]", @@ -618,9 +628,10 @@ export class JSONPythonRenderer extends PythonRenderer { (_integerType) => "int", (_doubleType) => "float", (_stringType) => "str", - (_arrayType) => this.pyOptions.features.builtinGenerics ? "list" : "List", + (_arrayType) => + this.pyOptions.features.builtinGenerics ? "list" : "List", (classType) => this.nameForNamedType(classType), - (_mapType) => this.pyOptions.features.builtinGenerics ? "dict" : "Dict", + (_mapType) => "dict", (enumType) => this.nameForNamedType(enumType), (_unionType) => undefined, (transformedStringType) => { diff --git a/packages/quicktype-core/src/language/Python/PythonRenderer.ts b/packages/quicktype-core/src/language/Python/PythonRenderer.ts index 0910f8f2d9..506ceea623 100644 --- a/packages/quicktype-core/src/language/Python/PythonRenderer.ts +++ b/packages/quicktype-core/src/language/Python/PythonRenderer.ts @@ -1,6 +1,7 @@ import { arrayIntercalate, iterableFirst, + iterableSome, mapSortBy, mapUpdateInto, setUnionInto, @@ -19,9 +20,11 @@ import { defined, panic } from "../../support/Support"; import type { TargetLanguage } from "../../TargetLanguage"; import { followTargetType } from "../../Transformers"; import { + ArrayType, type ClassProperty, ClassType, EnumType, + MapType, type Type, UnionType, } from "../../Type"; @@ -137,13 +140,89 @@ export class PythonRenderer extends ConvenienceRenderer { return this.withImport("typing", name); } - protected namedType(t: Type, _suppressQuotes: boolean = false): Sourcelike { + protected namedType(t: Type, suppressQuotes = false): Sourcelike { const name = this.nameForNamedType(t); - if (_suppressQuotes || this.declaredTypes.has(t)) return name; + if (suppressQuotes || this.declaredTypes.has(t)) return name; return ["'", name, "'"]; } - protected pythonType(t: Type, _isRootTypeDef = false, _suppressQuotes = false): Sourcelike { + // Would rendering `t` as a type annotation right now require a forward + // reference, i.e. does it mention a named type that hasn't been declared + // yet? + private typeContainsForwardRef(t: Type): boolean { + const actualType = followTargetType(t); + if (actualType instanceof ClassType || actualType instanceof EnumType) { + return !this.declaredTypes.has(actualType); + } + + if (actualType instanceof ArrayType) { + return this.typeContainsForwardRef(actualType.items); + } + + if (actualType instanceof MapType) { + return this.typeContainsForwardRef(actualType.values); + } + + if (actualType instanceof UnionType) { + return iterableSome(actualType.members, (m) => + this.typeContainsForwardRef(m), + ); + } + + return false; + } + + // Renders a union with PEP 604 syntax: `A | B | None`. A quoted forward + // reference is not allowed as an operand of `|` (`'A' | None` is a runtime + // `TypeError`), so if any member needs a forward reference we quote the + // whole union expression instead, suppressing the quotes on the individual + // names. A `" = None"` default, if required, must go outside the closing + // quote: `foo: 'Foo | None' = None`. + private pep604UnionType( + unionType: UnionType, + isRootTypeDef: boolean, + suppressQuotes: boolean, + ): Sourcelike { + const [hasNull, nonNulls] = removeNullFromUnion(unionType); + const needsQuotes = + !suppressQuotes && + iterableSome(nonNulls, (m) => this.typeContainsForwardRef(m)); + const quote = needsQuotes ? "'" : ""; + const memberTypes = Array.from(nonNulls).map((m) => + this.pythonType(m, false, true), + ); + + const union: Sourcelike[] = [ + quote, + arrayIntercalate(" | ", memberTypes), + ]; + if (hasNull !== null) { + union.push(" | None"); + } + + union.push(quote); + + if ( + hasNull !== null && + !this.getAlphabetizeProperties() && + (this.pyOptions.features.dataClasses || + this.pyOptions.pydanticBaseModel) && + isRootTypeDef + ) { + // Only push "= None" if this is a root level type def + // otherwise we may get type defs like list[int | None = None] + // which are invalid + union.push(" = None"); + } + + return union; + } + + protected pythonType( + t: Type, + _isRootTypeDef = false, + suppressQuotes = false, + ): Sourcelike { const actualType = followTargetType(t); return matchType( @@ -155,32 +234,37 @@ export class PythonRenderer extends ConvenienceRenderer { (_doubletype) => "float", (_stringType) => "str", (arrayType) => [ - this.pyOptions.features.builtinGenerics ? "list" : this.withTyping("List"), + this.pyOptions.features.builtinGenerics + ? "list" + : this.withTyping("List"), "[", - this.pythonType(arrayType.items), + this.pythonType(arrayType.items, false, suppressQuotes), "]", ], - (classType) => this.namedType(classType, _suppressQuotes), + (classType) => this.namedType(classType, suppressQuotes), (mapType) => [ - this.pyOptions.features.builtinGenerics ? "dict" : this.withTyping("Dict"), + this.pyOptions.features.builtinGenerics + ? "dict" + : this.withTyping("Dict"), "[str, ", - this.pythonType(mapType.values), + this.pythonType(mapType.values, false, suppressQuotes), "]", ], - (enumType) => this.namedType(enumType, _suppressQuotes), + (enumType) => this.namedType(enumType, suppressQuotes), (unionType) => { + if (this.pyOptions.features.unionOperators) { + return this.pep604UnionType( + unionType, + _isRootTypeDef, + suppressQuotes, + ); + } + const [hasNull, nonNulls] = removeNullFromUnion(unionType); - const hasClassOrEnumType = Array.from(nonNulls).some( - t => t instanceof ClassType || t instanceof EnumType - ); - const encapsulator = hasClassOrEnumType ? "'" : "" - const memberTypes = Array.from(nonNulls).map((m) => - // Suppress quotes for namedType - this.pythonType(m, false, true), + this.pythonType(m, false, suppressQuotes), ); - if (hasNull !== null) { const rest: string[] = []; if ( @@ -196,16 +280,6 @@ export class PythonRenderer extends ConvenienceRenderer { } if (nonNulls.size > 1) { - if (this.pyOptions.features.builtinGenerics) { - return [ - encapsulator, - arrayIntercalate(" | ", memberTypes), - " | None", - ...rest, - encapsulator - ]; - } - this.withImport("typing", "Union"); return [ this.withTyping("Optional"), @@ -216,15 +290,6 @@ export class PythonRenderer extends ConvenienceRenderer { ]; } - if (this.pyOptions.features.builtinGenerics) { - return [ - encapsulator, - defined(iterableFirst(memberTypes)), - " | None", - ...rest, - encapsulator - ]; - } return [ this.withTyping("Optional"), "[", @@ -234,14 +299,6 @@ export class PythonRenderer extends ConvenienceRenderer { ]; } - if (this.pyOptions.features.builtinGenerics) { - return [ - encapsulator, - arrayIntercalate(" | ", memberTypes), - encapsulator - ]; - } - return [ this.withTyping("Union"), "[", diff --git a/packages/quicktype-core/src/language/Python/language.ts b/packages/quicktype-core/src/language/Python/language.ts index 6bac0bb73f..42f389aa22 100644 --- a/packages/quicktype-core/src/language/Python/language.ts +++ b/packages/quicktype-core/src/language/Python/language.ts @@ -20,8 +20,12 @@ import { JSONPythonRenderer } from "./JSONPythonRenderer"; import { PythonRenderer } from "./PythonRenderer"; export interface PythonFeatures { + /** PEP 585 builtin generics (`list[str]`, `dict[str, int]`), Python 3.9+ */ + builtinGenerics: boolean; dataClasses: boolean; typeHints: boolean; + /** PEP 604 union operators (`str | None`), Python 3.10+ */ + unionOperators: boolean; } export const pythonOptions = { @@ -29,12 +33,38 @@ export const pythonOptions = { "python-version", "Python version", { - "3.5": { typeHints: false, dataClasses: false, builtinGenerics: false }, - "3.6": { typeHints: true, dataClasses: false, builtinGenerics: false }, - "3.7": { typeHints: true, dataClasses: true, builtinGenerics: false }, - "3.9": { typeHints: true, dataClasses: true, builtinGenerics: true }, - }, - "3.9", + "3.5": { + typeHints: false, + dataClasses: false, + builtinGenerics: false, + unionOperators: false, + }, + "3.6": { + typeHints: true, + dataClasses: false, + builtinGenerics: false, + unionOperators: false, + }, + "3.7": { + typeHints: true, + dataClasses: true, + builtinGenerics: false, + unionOperators: false, + }, + "3.9": { + typeHints: true, + dataClasses: true, + builtinGenerics: true, + unionOperators: false, + }, + "3.10": { + typeHints: true, + dataClasses: true, + builtinGenerics: true, + unionOperators: true, + }, + } satisfies Record, + "3.10", ), justTypes: new BooleanOption("just-types", "Classes only", false), nicePropertyNames: new BooleanOption( diff --git a/test/fixtures/python/run.sh b/test/fixtures/python/run.sh index 98c4273a0b..011cad464c 100755 --- a/test/fixtures/python/run.sh +++ b/test/fixtures/python/run.sh @@ -3,7 +3,7 @@ if [ "x$QUICKTYPE_PYTHON_VERSION" = "x2.7" ] ; then PYTHON="python2.7" else - PYTHON="python3.9" + PYTHON="python3.12" fi "$PYTHON" "$@" diff --git a/test/languages.ts b/test/languages.ts index 638c3a91d2..79f7046abf 100644 --- a/test/languages.ts +++ b/test/languages.ts @@ -263,7 +263,13 @@ export const PythonLanguage: Language = { "keyword-unions.schema", // Requires more than 255 arguments ], rendererOptions: {}, - quickTestRendererOptions: [{ "python-version": "3.5" }], + quickTestRendererOptions: [ + // The default is "3.10"; keep the older feature sets covered. + { "python-version": "3.5" }, + { "python-version": "3.6" }, + { "python-version": "3.7" }, + { "python-version": "3.9" }, + ], sourceFiles: ["src/language/Python/index.ts"], }; From 4c999957a1daeb8c84411fe38d4118bf86ba6895 Mon Sep 17 00:00:00 2001 From: Mark Probst Date: Sun, 19 Jul 2026 18:38:49 -0400 Subject: [PATCH 22/31] Make prefer-unknown the default for TypeScript and Flow The next release is a major, so the breaking output change is sanctioned: generated type declarations and converter signatures now use unknown (TypeScript) / mixed (Flow) instead of any by default, which also makes the standard typescript/flow fixtures exercise the new output across the whole corpus. Opt out with --no-prefer-unknown (CLI) or "prefer-unknown": false; the quick-test entries pin the legacy any output for both fixtures. Co-Authored-By: Claude Fable 5 --- .../src/language/JavaScript/JavaScriptRenderer.ts | 5 ++++- .../src/language/TypeScriptFlow/FlowRenderer.ts | 8 +++++++- .../src/language/TypeScriptFlow/language.ts | 2 +- test/languages.ts | 4 ++-- 4 files changed, 14 insertions(+), 5 deletions(-) diff --git a/packages/quicktype-core/src/language/JavaScript/JavaScriptRenderer.ts b/packages/quicktype-core/src/language/JavaScript/JavaScriptRenderer.ts index 0df9b8ccab..c14a44efdb 100644 --- a/packages/quicktype-core/src/language/JavaScript/JavaScriptRenderer.ts +++ b/packages/quicktype-core/src/language/JavaScript/JavaScriptRenderer.ts @@ -242,7 +242,10 @@ export class JavaScriptRenderer extends ConvenienceRenderer { /** The expression a deserializer returns when runtime typechecks * are disabled. Subclasses can wrap it in a cast to the target * type if `parsedJson`'s type isn't assignable to it. */ - protected uncheckedParsedJson(_t: Type, parsedJson: Sourcelike): Sourcelike { + protected uncheckedParsedJson( + _t: Type, + parsedJson: Sourcelike, + ): Sourcelike { return parsedJson; } diff --git a/packages/quicktype-core/src/language/TypeScriptFlow/FlowRenderer.ts b/packages/quicktype-core/src/language/TypeScriptFlow/FlowRenderer.ts index a67484fcda..9e48bfb147 100644 --- a/packages/quicktype-core/src/language/TypeScriptFlow/FlowRenderer.ts +++ b/packages/quicktype-core/src/language/TypeScriptFlow/FlowRenderer.ts @@ -25,7 +25,13 @@ export class FlowRenderer extends TypeScriptFlowBaseRenderer { this._tsFlowOptions.rawType !== "json" && this._tsFlowOptions.preferUnknown ) { - return ["((", parsedJson, ": any): ", this.sourceFor(t).source, ")"]; + return [ + "((", + parsedJson, + ": any): ", + this.sourceFor(t).source, + ")", + ]; } return parsedJson; diff --git a/packages/quicktype-core/src/language/TypeScriptFlow/language.ts b/packages/quicktype-core/src/language/TypeScriptFlow/language.ts index f45d9a97a5..a894ee2833 100644 --- a/packages/quicktype-core/src/language/TypeScriptFlow/language.ts +++ b/packages/quicktype-core/src/language/TypeScriptFlow/language.ts @@ -48,7 +48,7 @@ export const tsFlowOptions = { preferUnknown: new BooleanOption( "prefer-unknown", "Use unknown (TypeScript) or mixed (Flow) instead of any", - false, + true, ), }; diff --git a/test/languages.ts b/test/languages.ts index ec3f6a7409..07f9a417a1 100644 --- a/test/languages.ts +++ b/test/languages.ts @@ -864,7 +864,7 @@ export const TypeScriptLanguage: Language = { { "acronym-style": "pascal" }, { converters: "all-objects" }, { readonly: "true" }, - { "prefer-unknown": "true" }, + { "prefer-unknown": "false" }, ], sourceFiles: ["src/language/TypeScript/index.ts"], }; @@ -944,7 +944,7 @@ export const FlowLanguage: Language = { { "runtime-typecheck": "false" }, { "runtime-typecheck-ignore-unknown-properties": "true" }, { "nice-property-names": "true" }, - { "prefer-unknown": "true" }, + { "prefer-unknown": "false" }, ], sourceFiles: ["src/language/Flow/index.ts"], }; From b3cb535a4b90d6a1ad5c19a764690427d77fc1f0 Mon Sep 17 00:00:00 2001 From: Mark Probst Date: Sun, 19 Jul 2026 18:41:46 -0400 Subject: [PATCH 23/31] C#: also suppress CS8765 for Newtonsoft at csharp-version 8 The generated Newtonsoft JsonConverter overrides declare their parameters as non-nullable "object" while Newtonsoft.Json 13 annotates them as "object?", which produces CS8765 warnings under "#nullable enable". "dotnet run" prints warnings to stdout, which would break the fixture round-trips. Verified with the .NET 8 SDK: with this suppression every priority/samples JSON input compiles with zero warnings at csharp-version 8 for both the Newtonsoft and System.Text.Json renderers. Co-Authored-By: Claude Fable 5 --- .../src/language/CSharp/NewtonSoftCSharpRenderer.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/quicktype-core/src/language/CSharp/NewtonSoftCSharpRenderer.ts b/packages/quicktype-core/src/language/CSharp/NewtonSoftCSharpRenderer.ts index b4e325988f..1993f6e135 100644 --- a/packages/quicktype-core/src/language/CSharp/NewtonSoftCSharpRenderer.ts +++ b/packages/quicktype-core/src/language/CSharp/NewtonSoftCSharpRenderer.ts @@ -198,6 +198,7 @@ export class NewtonsoftCSharpRenderer extends CSharpRenderer { this.emitLine("#pragma warning restore CS8618"); this.emitLine("#pragma warning restore CS8601"); this.emitLine("#pragma warning restore CS8603"); + this.emitLine("#pragma warning restore CS8765"); } protected emitDefaultLeadingComments(): void { @@ -239,6 +240,7 @@ export class NewtonsoftCSharpRenderer extends CSharpRenderer { this.emitLine("#pragma warning disable CS8618"); this.emitLine("#pragma warning disable CS8601"); this.emitLine("#pragma warning disable CS8603"); + this.emitLine("#pragma warning disable CS8765"); } } From 96f3acca3c42fcab6684dfe428b1b726f5ea6020 Mon Sep 17 00:00:00 2001 From: Mark Probst Date: Sun, 19 Jul 2026 18:50:58 -0400 Subject: [PATCH 24/31] Give = None defaults to Any/None dataclass fields MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit With dataclasses (3.7+), optional properties become nullable unions and get a " = None" default — except when the property type is Any (null is absorbed into it) or plain null (the union collapses), which got no default yet could sort after defaulted fields, making the generated dataclass raise "TypeError: non-default argument follows default argument" at import. This was latent on master because CI only ever exercised 3.5/3.6, which don't use dataclasses; with 3.10 as the new default it breaks e.g. vega-lite.schema and the combinations samples. Emit " = None" for root-level Any/None fields too, and make the property sort order compute exactly "will this field render with a default" via followTargetType — the old kind-based predicate saw pre-transformation types (transformed unions have kind "any" at sort time) and could order a non-defaulted field after defaulted ones. Co-Authored-By: Claude Fable 5 --- .../src/language/Python/PythonRenderer.ts | 78 +++++++++++-------- 1 file changed, 44 insertions(+), 34 deletions(-) diff --git a/packages/quicktype-core/src/language/Python/PythonRenderer.ts b/packages/quicktype-core/src/language/Python/PythonRenderer.ts index f21101c7b6..e0d60263ab 100644 --- a/packages/quicktype-core/src/language/Python/PythonRenderer.ts +++ b/packages/quicktype-core/src/language/Python/PythonRenderer.ts @@ -28,11 +28,7 @@ import { type Type, UnionType, } from "../../Type/index.js"; -import { - matchType, - nullableFromUnion, - removeNullFromUnion, -} from "../../Type/TypeUtils.js"; +import { matchType, removeNullFromUnion } from "../../Type/TypeUtils.js"; import { forbiddenPropertyNames, forbiddenTypeNames } from "./constants.js"; import type { pythonOptions } from "./language.js"; @@ -202,20 +198,44 @@ export class PythonRenderer extends ConvenienceRenderer { union.push(quote); + if (hasNull !== null) { + union.push(...this.noneDefault(isRootTypeDef)); + } + + return union; + } + + // A `" = None"` default for a class property whose value can be `None`. + // Only emitted for root level type defs, otherwise we may get type defs + // like `List[Optional[int] = None]`, which are invalid. Every property + // that gets a default must sort after all properties that don't — see + // `sortClassProperties`. + private noneDefault(isRootTypeDef: boolean): string[] { if ( - hasNull !== null && + isRootTypeDef && !this.getAlphabetizeProperties() && (this.pyOptions.features.dataClasses || - this.pyOptions.pydanticBaseModel) && - isRootTypeDef + this.pyOptions.pydanticBaseModel) ) { - // Only push "= None" if this is a root level type def - // otherwise we may get type defs like list[int | None = None] - // which are invalid - union.push(" = None"); + return [" = None"]; } - return union; + return []; + } + + // Does `pythonType(p.type, true)` end in a `" = None"` default? This + // must mirror the `noneDefault` calls in `pythonType` exactly: nullable + // unions, plus `Any` and `None` typed properties — an optional `Any` + // stays `Any` (`null` is absorbed by it), and an optional `null` + // collapses to just `null`, so those also default to `None`. + private classPropertyHasNoneDefault(p: ClassProperty): boolean { + const actualType = followTargetType(p.type); + if (actualType instanceof UnionType) { + const [hasNull] = removeNullFromUnion(actualType); + return hasNull !== null; + } + + return actualType.kind === "any" || actualType.kind === "null"; } protected pythonType( @@ -227,8 +247,11 @@ export class PythonRenderer extends ConvenienceRenderer { return matchType( actualType, - (_anyType) => this.withTyping("Any"), - (_nullType) => "None", + (_anyType) => [ + this.withTyping("Any"), + ...this.noneDefault(_isRootTypeDef), + ], + (_nullType) => ["None", ...this.noneDefault(_isRootTypeDef)], (_boolType) => "bool", (_integerType) => "int", (_doubletype) => "float", @@ -266,18 +289,7 @@ export class PythonRenderer extends ConvenienceRenderer { ); if (hasNull !== null) { - const rest: string[] = []; - if ( - !this.getAlphabetizeProperties() && - (this.pyOptions.features.dataClasses || - this.pyOptions.pydanticBaseModel) && - _isRootTypeDef - ) { - // Only push "= None" if this is a root level type def - // otherwise we may get type defs like List[Optional[int] = None] - // which are invalid - rest.push(" = None"); - } + const rest = this.noneDefault(_isRootTypeDef); if (nonNulls.size > 1) { this.withImport("typing", "Union"); @@ -412,13 +424,11 @@ export class PythonRenderer extends ConvenienceRenderer { this.pyOptions.features.dataClasses || this.pyOptions.pydanticBaseModel ) { - return mapSortBy(properties, (p: ClassProperty) => { - return (p.type instanceof UnionType && - nullableFromUnion(p.type) !== null) || - p.isOptional - ? 1 - : 0; - }); + // 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, + ); } return super.sortClassProperties(properties, propertyNames); From 6022de7f3c9b9ca415f4f0d897f78afb7436b2e2 Mon Sep 17 00:00:00 2001 From: Mark Probst Date: Sun, 19 Jul 2026 20:59:20 -0400 Subject: [PATCH 25/31] scala3: real enum codecs, safer union decoding, top-level primitives Enum cases are now styled Scala identifiers (via a proper enum-case namer) with explicit Decoder/Encoder mappings back to the original JSON strings, so keywords, "_", "", and other arbitrary values all work. This subsumes the special-cased blank-enum emission, which produced Decoder[""] -- a literal singleton type that plain circe has no decoder for -- alongside a commented-out generic given. Union decoders are tried most-discriminating-first (enums, then strings, then the remaining primitives, classes last) via a shared sort order in utils, because circe's numeric decoders also accept strings like "5". Scala.Right/scala.Left are fully qualified in the enum codecs so that generated types named Right/Left don't shadow them, property names containing spaces are backtick-quoted (one shared backtickedName helper replaces the copy-pasted quoting logic), and top-level primitives emit a type alias so they compile at all. Co-Authored-By: Claude Fable 5 --- .../src/language/Scala3/CirceRenderer.ts | 195 ++++++------------ .../src/language/Scala3/Scala3Renderer.ts | 47 +---- .../src/language/Scala3/utils.ts | 58 +++++- 3 files changed, 118 insertions(+), 182 deletions(-) diff --git a/packages/quicktype-core/src/language/Scala3/CirceRenderer.ts b/packages/quicktype-core/src/language/Scala3/CirceRenderer.ts index 5e6419eb50..fc4a23dd39 100644 --- a/packages/quicktype-core/src/language/Scala3/CirceRenderer.ts +++ b/packages/quicktype-core/src/language/Scala3/CirceRenderer.ts @@ -14,8 +14,10 @@ import type { UnionType, } from "../../Type/index.js"; +import { stringEscape } from "../../support/Strings.js"; + import { Scala3Renderer } from "./Scala3Renderer.js"; -import { shouldAddBacktick, wrapOption } from "./utils.js"; +import { unionMemberSortOrder, wrapOption } from "./utils.js"; export class CirceRenderer extends Scala3Renderer { private readonly seenUnionTypes: string[] = []; @@ -109,147 +111,70 @@ export class CirceRenderer extends Scala3Renderer { protected emitEnumDefinition(e: EnumType, enumName: Name): void { this.emitDescription(this.descriptionForType(e)); - - let hasBlank = false; - this.forEachEnumCase(e, "none", (_, jsonName) => { - if (jsonName.trim() === "") { - hasBlank = true; - } - }); this.ensureBlankLine(); - const isBlank = (str: string): boolean => !str.trim(); - - if (hasBlank) { - //console.log("enumName: " + enumName + " has blank"); - this.emitItem(["type ", enumName, ' = "" | ', enumName, "NonBlank"]); - this.ensureBlankLine(); - this.emitLine([ - "given ", - enumName, - "Enc: Encoder[", - enumName, - "] = Encoder.encodeString.contramap(_.toString())", - ]); - this.emitLine([ - "given ", - enumName, - "Dec: Decoder[", - enumName, - "] = List[Decoder[", - enumName, - "]](", - ]); - this.indent(() => { - this.emitLine('Decoder[""].widen,'); - this.emitLine(["Decoder[", enumName, "NonBlank].widen"]); + // Enum cases are styled Scala identifiers; the codecs below map + // them back to the original JSON strings, which can be anything + // (keywords, `"_"`, `""`, …). + this.emitLine(["enum ", enumName, " : "]); + this.indent(() => { + this.forEachEnumCase(e, "none", (name) => { + this.emitLine("case ", name); }); - this.emitLine(").reduceLeft(_ or _)"); - - this.ensureBlankLine(); - //let count = e.cases.size; - this.ensureBlankLine(); - this.emitLine(["enum ", enumName, "NonBlank :"]); - this.indent(() => { - //let count = e.cases.size; - - this.forEachEnumCase(e, "none", (_, jsonName) => { - let strBuild = ""; - const backticks = - shouldAddBacktick(jsonName) || - jsonName.includes(" ") || - !Number.isNaN(Number.parseInt(jsonName.charAt(0))); - if (!isBlank(jsonName)) { - this.emitItem(["case "]); - } - - if (backticks) { - strBuild = strBuild + "`"; - } - - strBuild = strBuild + jsonName; - if (backticks) { - strBuild = strBuild + "`"; - } + }); + this.ensureBlankLine(); - //if (--count > 0) strBuild + ","; - // don't emit the blank case - if (!isBlank(jsonName)) { - this.emitLine([strBuild]); - } - }); + this.emitLine([ + "given Decoder[", + enumName, + "] = Decoder.decodeString.emap {", + ]); + this.indent(() => { + // `scala.` in case a generated type is named Right/Left. + this.forEachEnumCase(e, "none", (name, jsonName) => { + this.emitLine([ + `case "${stringEscape(jsonName)}" => scala.Right(`, + enumName, + ".", + name, + ")", + ]); }); - this.emitLine([ - "given Decoder[", - enumName, - "NonBlank] = Decoder.decodeString.emapTry(x => Try(", + 'case other => scala.Left("invalid ', enumName, - "NonBlank.valueOf(x) ))", + ': " + other)', ]); - this.emitLine( - "given Encoder[", - enumName, - "NonBlank] = Encoder.encodeString.contramap(_.toString())", - ); - } else { - //console.log("enumName: " + enumName + " has non blank"); - this.emitLine(["enum ", enumName, " : "]); - - this.indent(() => { - this.forEachEnumCase(e, "none", (_, jsonName) => { - let strBuild = ""; - const backticks = - shouldAddBacktick(jsonName) || - jsonName.includes(" ") || - !Number.isNaN(Number.parseInt(jsonName.charAt(0))); - this.emitItem(["case "]); - if (backticks) { - strBuild = strBuild + "`"; - } - - strBuild = strBuild + jsonName; - if (backticks) { - strBuild = strBuild + "`"; - } - - // if (--count > 0) strBuild + ","; - this.emitLine([strBuild]); - }); + }); + this.emitLine("}"); + this.emitLine([ + "given Encoder[", + enumName, + "] = Encoder.encodeString.contramap {", + ]); + this.indent(() => { + this.forEachEnumCase(e, "none", (name, jsonName) => { + this.emitLine([ + "case ", + enumName, + ".", + name, + ` => "${stringEscape(jsonName)}"`, + ]); }); - this.emitLine([ - "given Decoder[", - enumName, - "] = Decoder.decodeString.emapTry(x => Try(", - enumName, - ".valueOf(x) )) ", - ]); - this.emitLine([ - "given Encoder[", - enumName, - "] = Encoder.encodeString.contramap(_.toString())", - ]); - this.ensureBlankLine(); - } + }); + this.emitLine("}"); + this.ensureBlankLine(); } protected emitHeader(): void { super.emitHeader(); - this.emitLine("import scala.util.Try"); this.emitLine("import io.circe.syntax._"); this.emitLine("import io.circe._"); this.emitLine("import cats.syntax.functor._"); this.ensureBlankLine(); - this.emitLine("// For serialising string unions"); - // this.emitLine( - // "given [A <: Singleton](using A <:< String): Decoder[A] = Decoder.decodeString.emapTry(x => Try(x.asInstanceOf[A])) " - // ); - // this.emitLine( - // "given [A <: Singleton](using ev: A <:< String): Encoder[A] = Encoder.encodeString.contramap(ev) " - // ); - this.ensureBlankLine(); this.emitLine( "// If a union has a null in, then we'll need this too... ", ); @@ -286,15 +211,9 @@ export class CirceRenderer extends Scala3Renderer { } protected emitUnionDefinition(u: UnionType, unionName: Name): void { - function sortBy(t: Type): string { - const kind = t.kind; - if (kind === "class") return kind; - return `_${kind}`; - } - this.emitDescription(this.descriptionForType(u)); - const [maybeNull, nonNulls] = removeNullFromUnion(u, sortBy); + const [maybeNull, nonNulls] = removeNullFromUnion(u, false); const theTypes: Sourcelike[] = []; this.forEachUnionMember(u, nonNulls, "none", null, (_, t) => { theTypes.push(this.scalaType(t)); @@ -314,10 +233,20 @@ export class CirceRenderer extends Scala3Renderer { this.ensureBlankLine(); if (!this.seenUnionTypes.some((y) => y === thisUnionType)) { this.seenUnionTypes.push(thisUnionType); + // The decoders are tried in order, most discriminating + // first (see `unionMemberSortOrder`): circe's numeric + // decoders accept strings like "5", so the string decoder + // has to get a shot before them. const sourceLikeTypes: Array<[Sourcelike, Type]> = []; - this.forEachUnionMember(u, nonNulls, "none", null, (_, t) => { - sourceLikeTypes.push([this.scalaType(t), t]); - }); + this.forEachUnionMember( + u, + nonNulls, + "none", + unionMemberSortOrder, + (_, t) => { + sourceLikeTypes.push([this.scalaType(t), t]); + }, + ); if (maybeNull !== null) { sourceLikeTypes.push([ this.nameForUnionMember(u, maybeNull), diff --git a/packages/quicktype-core/src/language/Scala3/Scala3Renderer.ts b/packages/quicktype-core/src/language/Scala3/Scala3Renderer.ts index 061acb2f93..09b6e59fcf 100644 --- a/packages/quicktype-core/src/language/Scala3/Scala3Renderer.ts +++ b/packages/quicktype-core/src/language/Scala3/Scala3Renderer.ts @@ -30,9 +30,10 @@ import { import { keywords } from "./constants.js"; import type { scala3Options } from "./language.js"; import { + backtickedName, + enumCaseNameStyle, lowerNamingFunction, scalaNameStyle, - shouldAddBacktick, upperNamingFunction, wrapOption, } from "./utils.js"; @@ -91,7 +92,7 @@ export class Scala3Renderer extends ConvenienceRenderer { } protected makeEnumCaseNamer(): Namer { - return funPrefixNamer("upper", (s) => s.replace(" ", "")); // TODO - add backticks where appropriate + return funPrefixNamer("upper", enumCaseNameStyle); } protected emitDescriptionBlock(lines: Sourcelike[]): void { @@ -238,7 +239,6 @@ export class Scala3Renderer extends ConvenienceRenderer { let count = c.getProperties().size; let first = true; this.forEachClassProperty(c, "none", (_, jsonName, p) => { - //console.log(jsonName); // Why is this in different order!!!!!! const nullable = p.type.kind === "union" && nullableFromUnion(p.type as UnionType) !== null; @@ -263,14 +263,9 @@ export class Scala3Renderer extends ConvenienceRenderer { emit(); } - const nameNeedsBackticks = - jsonName.endsWith("_") || shouldAddBacktick(jsonName); - const nameWithBackticks = nameNeedsBackticks - ? `\`${jsonName}\`` - : jsonName; this.emitLine( "val ", - nameWithBackticks, + backtickedName(jsonName), " : ", scalaType(p), p.isOptional @@ -297,39 +292,13 @@ export class Scala3Renderer extends ConvenienceRenderer { } protected emitEnumDefinition(e: EnumType, enumName: Name): void { - //console.log("vanilla"); this.emitDescription(this.descriptionForType(e)); this.emitBlock( ["enum ", enumName, " : "], () => { - let count = e.cases.size; - if (count > 0) { - this.emitItem("\t case "); - } - - this.forEachEnumCase(e, "none", (name, jsonName) => { - //console.log(jsonName); - if (!(jsonName === "")) { - const backticks = - shouldAddBacktick(jsonName) || - jsonName.includes(" ") || - !Number.isNaN( - Number.parseInt(jsonName.charAt(0), 10), - ); - if (backticks) { - this.emitItem("`"); - } - - this.emitItemOnce([name]); - if (backticks) { - this.emitItem("`"); - } - - if (--count > 0) this.emitItem([","]); - } else { - --count; - } + this.forEachEnumCase(e, "none", (name) => { + this.emitLine("case ", name); }); }, "none", @@ -364,12 +333,14 @@ export class Scala3Renderer extends ConvenienceRenderer { protected emitSourceStructure(): void { this.emitHeader(); - // Top-level arrays, maps + // Top-level arrays, maps, and primitives this.forEachTopLevel("leading", (t, name) => { if (t instanceof ArrayType) { this.emitTopLevelArray(t, name); } else if (t instanceof MapType) { this.emitTopLevelMap(t, name); + } else if (t.isPrimitive()) { + this.emitLine(["type ", name, " = ", this.scalaType(t)]); } }); diff --git a/packages/quicktype-core/src/language/Scala3/utils.ts b/packages/quicktype-core/src/language/Scala3/utils.ts index 3c7258dc44..638855d9b7 100644 --- a/packages/quicktype-core/src/language/Scala3/utils.ts +++ b/packages/quicktype-core/src/language/Scala3/utils.ts @@ -1,4 +1,5 @@ -import { funPrefixNamer } from "../../Naming.js"; +import { type Name, funPrefixNamer } from "../../Naming.js"; +import type { Type } from "../../Type/index.js"; import { allLowerWordStyle, allUpperWordStyle, @@ -26,6 +27,15 @@ export const shouldAddBacktick = (paramName: string): boolean => { ); }; +/** + * Wrap a name in backticks if it isn't usable as a plain Scala identifier. + */ +export const backtickedName = (name: string): string => { + return name.endsWith("_") || name.includes(" ") || shouldAddBacktick(name) + ? `\`${name}\`` + : name; +}; + export const wrapOption = (s: string, optional: boolean): string => { if (optional) { return `Option[${s}]`; @@ -34,6 +44,29 @@ export const wrapOption = (s: string, optional: boolean): string => { } }; +/** + * Sort order for union members when emitting the decoders of an untagged + * union, which try each member in turn: both circe's and upickle's + * primitive number/boolean decoders are lenient (they accept strings like + * "5"), and a case class whose fields are all defaulted can spuriously + * read non-object JSON with upickle, so try the most discriminating + * decoders first -- enums (which accept only their known strings), then + * strings, then the other primitives, classes last. + */ +export const unionMemberSortOrder = (_: Name, t: Type): string => { + const priority: Partial> = { + enum: "0", + string: "2", + bool: "3", + integer: "4", + double: "5", + array: "6", + map: "7", + class: "9", + }; + return `${priority[t.kind] ?? "8"}${t.kind}`; +}; + function isPartCharacter(codePoint: number): boolean { return isLetterOrUnderscore(codePoint) || isNumeric(codePoint); } @@ -58,16 +91,19 @@ export function scalaNameStyle(isUpper: boolean, original: string): string { ); } -/* function unicodeEscape(codePoint: number): string { - return "\\u" + intToHex(codePoint, 4); -} */ - -// const _stringEscape = utf32ConcatMap(escapeNonPrintableMapper(isPrintable, unicodeEscape)); - -/* function stringEscape(s: string): string { - // "$this" is a template string in Kotlin so we have to escape $ - return _stringEscape(s).replace(/\$/g, "\\$"); -} */ +/** + * Style an enum case as a legal Scala identifier. The JSON string an enum + * case comes from can be anything (a keyword, `"_"`, `""`, …), so the + * renderers emit styled case names and map them back to the original JSON + * strings in their codecs. + */ +export function enumCaseNameStyle(original: string): string { + const styled = scalaNameStyle(true, original); + // `scalaNameStyle` can produce the empty string, for example for `""` + // or `"_"` (which is not a legal identifier even in backticks). The + // namer disambiguates if an enum has several such cases. + return styled === "" ? "Empty" : styled; +} export const upperNamingFunction = funPrefixNamer("upper", (s) => scalaNameStyle(true, s), From ba661fe1d350e0f1749490c37468eb8efef6687e Mon Sep 17 00:00:00 2001 From: Mark Probst Date: Sun, 19 Jul 2026 20:59:33 -0400 Subject: [PATCH 26/31] scala3: fix the upickle renderer's unions, enums, and null members The upickle rewrite from PR #2246 (OptionPickler for absent/null options, badMerge for untagged unions) had several bugs that were never caught because the fixture was unregistered: - Union readers were tried in name order, so a case class whose fields are all defaulted would spuriously read strings and arrays as the all-defaults instance. Readers now run most-discriminating-first (shared sort order with the circe renderer). - upickle's built-in primitive readers are lenient in both directions (numbers read as strings, strings read as numbers/booleans), so unions now use strict per-type readers emitted in JsonExt. - The union writer used OptionPickler.write (a String) inside comap[ujson.Value], double-encoding every union value; it now uses writeJs. - Unions containing null referenced a NullValue type that was never emitted; the header now defines it with a strict ReadWriter. - Enums relied on raw JSON strings as case names; they are now styled identifiers with an explicit bimap ReadWriter mapping them back to the original strings. - The unused upickleEncoderForType, the unused widen extension, the commented-out singleton pickler, and the mislabeled unionReader/unionWriter given names are cleaned up. Co-Authored-By: Claude Fable 5 --- .../src/language/Scala3/UpickleRenderer.ts | 315 +++++++----------- 1 file changed, 117 insertions(+), 198 deletions(-) diff --git a/packages/quicktype-core/src/language/Scala3/UpickleRenderer.ts b/packages/quicktype-core/src/language/Scala3/UpickleRenderer.ts index 0216913d19..bc274f3c4d 100644 --- a/packages/quicktype-core/src/language/Scala3/UpickleRenderer.ts +++ b/packages/quicktype-core/src/language/Scala3/UpickleRenderer.ts @@ -1,83 +1,17 @@ import type { Name } from "../../Naming.js"; import type { Sourcelike } from "../../Source.js"; -import { - matchType, - nullableFromUnion, - removeNullFromUnion, -} from "../../Type/TypeUtils.js"; +import { removeNullFromUnion } from "../../Type/TypeUtils.js"; import type { ClassType, EnumType, Type, UnionType } from "../../Type/index.js"; +import { stringEscape } from "../../support/Strings.js"; import { Scala3Renderer } from "./Scala3Renderer.js"; -import { shouldAddBacktick, wrapOption } from "./utils.js"; +import { unionMemberSortOrder, wrapOption } from "./utils.js"; export class UpickleRenderer extends Scala3Renderer { - private seenUnionTypes: string[] = []; - - protected upickleEncoderForType( - t: Type, - __ = false, - noOptional = false, - paramName = "", - ): Sourcelike { - return matchType( - t, - (_anyType) => ["OptionPickler.writeJs(", paramName, ")"], - (_nullType) => ["OptionPickler.writeJs(", paramName, ")"], - (_boolType) => ["OptionPickler.writeJs(", paramName, ")"], - (_integerType) => ["OptionPickler.writeJs(", paramName, ")"], - (_doubleType) => ["OptionPickler.writeJs(", paramName, ")"], - (_stringType) => ["OptionPickler.writeJs(", paramName, ")"], - (arrayType) => [ - "OptionPickler.writeJs[", - this.scalaType(arrayType.items), - "].apply(", - paramName, - ")", - ], - (classType) => [ - "OptionPickler.writeJs[", - this.scalaType(classType), - "].apply(", - paramName, - ")", - ], - (mapType) => [ - "OptionPickler.writeJs[String,", - this.scalaType(mapType.values), - "].apply(", - paramName, - ")", - ], - (_) => ["OptionPickler.writeJs(", paramName, ")"], - (unionType) => { - const nullable = nullableFromUnion(unionType); - if (nullable !== null) { - if (noOptional) { - return [ - "OptionPickler.writeJs[", - this.nameForNamedType(nullable), - "]", - ]; - } - - return [ - "OptionPickler.writeJs[Option[", - this.nameForNamedType(nullable), - "]]", - ]; - } - - return [ - "OptionPickler.writeJs[", - this.nameForNamedType(unionType), - "]", - ]; - }, - ); - } + private readonly seenUnionTypes: string[] = []; protected emitClassDefinitionMethods(): void { - this.emitLine(") derives OptionPickler.ReadWriter "); + this.emitLine(") derives OptionPickler.ReadWriter"); } protected anySourceType(optional: boolean): Sourcelike { @@ -86,7 +20,10 @@ export class UpickleRenderer extends Scala3Renderer { protected emitHeader(): void { super.emitHeader(); - const optionPickler = `object OptionPickler extends upickle.AttributeTagged : + this.emitMultiline(`// Custom pickler so that missing keys and JSON nulls both read as None, +// and None is left out when writing (upickle's default for Option is a +// JSON array). +object OptionPickler extends upickle.AttributeTagged: import upickle.default.Writer import upickle.default.Reader override implicit def OptionWriter[T: Writer]: Writer[Option[T]] = @@ -102,8 +39,37 @@ export class UpickleRenderer extends Scala3Renderer { } end OptionPickler +// If a union has a null in, then we'll need this too... +type NullValue = None.type +given OptionPickler.ReadWriter[NullValue] = OptionPickler.readwriter[ujson.Value].bimap[NullValue]( + _ => ujson.Null, + json => if json.isNull then None else throw new upickle.core.Abort("not null") +) + object JsonExt: val valueReader = OptionPickler.readwriter[ujson.Value] + + // upickle's built-in primitive readers are lenient -- the numeric and + // boolean readers accept strings, and the string reader accepts + // numbers and booleans -- so untagged unions need strict readers to + // pick the right member. + val strictString: OptionPickler.Reader[String] = valueReader.map { + case ujson.Str(s) => s + case json => throw new upickle.core.Abort("expected string, got " + json) + } + val strictLong: OptionPickler.Reader[Long] = valueReader.map { + case ujson.Num(n) if n.isWhole => n.toLong + case json => throw new upickle.core.Abort("expected integer, got " + json) + } + val strictDouble: OptionPickler.Reader[Double] = valueReader.map { + case ujson.Num(n) => n + case json => throw new upickle.core.Abort("expected number, got " + json) + } + val strictBoolean: OptionPickler.Reader[Boolean] = valueReader.map { + case ujson.Bool(b) => b + case json => throw new upickle.core.Abort("expected boolean, got " + json) + } + def badMerge[T](r1: => OptionPickler.Reader[?], rest: OptionPickler.Reader[?]*): OptionPickler.Reader[T] = valueReader.map { json => var t: T | Null = null val stack = Vector.newBuilder[Throwable] @@ -116,19 +82,9 @@ object JsonExt: } if t != null then t.nn else throw new Exception(json.toString(), stack.result().headOption.getOrElse(null)) } - - extension [T](r: OptionPickler.Reader[T]) def widen[K >: T] = r.map(_.asInstanceOf[K]) end JsonExt -`; - // const singletonPickler = `given singletonStringPickler[A <: Singleton](using A <:< String): OptionPickler.ReadWriter[A] = OptionPickler.readwriter[ujson.Value].bimap[A]( - // _.toString(), - // str => { - // str.toString().asInstanceOf[A]() - // } - // )`; - this.emitMultiline(optionPickler); +`); this.ensureBlankLine(); - // this.emitMultiline(singletonPickler); } protected override emitEmptyClassDefinition( @@ -144,12 +100,6 @@ end JsonExt u: UnionType, unionName: Name, ): void { - // function sortBy(t: Type): string { - // const kind = t.kind; - // if (kind === "class") return kind; - // return "_" + kind; - // } - this.emitDescription(this.descriptionForType(u)); const [maybeNull, nonNulls] = removeNullFromUnion(u, false); @@ -173,9 +123,15 @@ end JsonExt if (!this.seenUnionTypes.some((y) => y === thisUnionType)) { this.seenUnionTypes.push(thisUnionType); const sourceLikeTypes: Array<[Sourcelike, Type]> = []; - this.forEachUnionMember(u, nonNulls, "none", null, (_, t) => { - sourceLikeTypes.push([this.scalaType(t), t]); - }); + this.forEachUnionMember( + u, + nonNulls, + "none", + unionMemberSortOrder, + (_, t) => { + sourceLikeTypes.push([this.scalaType(t), t]); + }, + ); if (maybeNull !== null) { sourceLikeTypes.push([ this.nameForUnionMember(u, maybeNull), @@ -185,7 +141,7 @@ end JsonExt this.ensureBlankLine(); this.emitLine([ - "given unionWriter", + "given unionReader", unionName, ": OptionPickler.Reader[", unionName, @@ -194,18 +150,34 @@ end JsonExt "](", ]); this.indent(() => { - sourceLikeTypes.forEach((t) => { - this.emitLine([ - "summon[OptionPickler.Reader[", - t[0], - "]],", - ]); + sourceLikeTypes.forEach(([srcType, t]) => { + // Use the strict readers for primitive members -- + // upickle's built-in ones accept too much (see the + // comment in the emitted JsonExt). + const strictReaders: Partial> = { + Boolean: "JsonExt.strictBoolean", + Double: "JsonExt.strictDouble", + Long: "JsonExt.strictLong", + String: "JsonExt.strictString", + }; + const strict = t.isPrimitive() + ? strictReaders[this.sourcelikeToString(srcType)] + : undefined; + if (strict === undefined) { + this.emitLine([ + "summon[OptionPickler.Reader[", + srcType, + "]],", + ]); + } else { + this.emitLine([strict, ","]); + } }); this.emitLine(")"); }); this.ensureBlankLine(); this.emitLine([ - "given unionReader", + "given unionWriter", unionName, ": OptionPickler.Writer[", unionName, @@ -220,7 +192,7 @@ end JsonExt this.emitLine([ "case v: ", t[0], - " => OptionPickler.write[", + " => OptionPickler.writeJs[", t[0], "](v)", ]); @@ -233,112 +205,59 @@ end JsonExt protected emitEnumDefinition(e: EnumType, enumName: Name): void { this.emitDescription(this.descriptionForType(e)); + this.ensureBlankLine(); - let hasBlank = false; - this.forEachEnumCase(e, "none", (_, jsonName) => { - if (jsonName.trim() === "") { - hasBlank = true; - } + // Enum cases are styled Scala identifiers; the ReadWriter below + // maps them back to the original JSON strings, which can be + // anything (keywords, `"_"`, `""`, …). + this.emitLine(["enum ", enumName, " : "]); + this.indent(() => { + this.forEachEnumCase(e, "none", (name) => { + this.emitLine("case ", name); + }); }); this.ensureBlankLine(); - if (hasBlank) { - //console.log("enumName: " + enumName + " has blank"); - this.emitItem(["type ", enumName, ' = "" | ', enumName, "NonBlank"]); - this.ensureBlankLine(); - this.emitLine([ - "given singleton", - enumName, - 'Pickler[A <: "" | ', - enumName, - "NonBlank]: OptionPickler.ReadWriter[A] = ", - ]); + this.emitLine([ + "given OptionPickler.ReadWriter[", + enumName, + "] = OptionPickler.readwriter[String].bimap[", + enumName, + "](", + ]); + this.indent(() => { + this.emitLine("{"); this.indent(() => { - this.emitLine([ - "OptionPickler.readwriter[ujson.Value].bimap[A](", - ]); - this.indent(() => { - this.emitLine(["_.toString(),"]); - this.emitLine(["str => {"]); - this.indent(() => { - this.emitLine(["val str2 = str.str"]); - this.emitLine(["str2 match {"]); - this.indent(() => { - this.emitLine([ - 'case _ if str2.length == 0 => "".asInstanceOf[A] ', - ]); - this.emitLine([ - "case parseable =>", - enumName, - "NonBlank.valueOf(parseable).asInstanceOf[A] ", - ]); - }); - this.emitLine(["}"]); - }); - this.emitLine(["}"]); - }); - this.emitLine([")"]); - }); - this.ensureBlankLine(); - //let count = e.cases.size; - this.ensureBlankLine(); - this.emitLine([ - "enum ", - enumName, - "NonBlank derives OptionPickler.ReadWriter: ", - ]); - this.indent(() => { - this.forEachEnumCase(e, "none", (_, jsonName) => { - if (!(jsonName.trim() === "")) { - let strBuild = ""; - const backticks = - shouldAddBacktick(jsonName) || - jsonName.includes(" ") || - !Number.isNaN(Number.parseInt(jsonName.charAt(0))); - this.emitItem(["case "]); - if (backticks) { - strBuild = strBuild + "`"; - } - - strBuild = strBuild + jsonName; - if (backticks) { - strBuild = strBuild + "`"; - } - - // if (--count > 0) strBuild + ","; - this.emitLine([strBuild]); - } + this.forEachEnumCase(e, "none", (name, jsonName) => { + this.emitLine([ + "case ", + enumName, + ".", + name, + ` => "${stringEscape(jsonName)}"`, + ]); }); }); - } else { - //console.log("enumName: " + enumName + " has non blank"); - this.emitLine([ - "enum ", - enumName, - " derives OptionPickler.ReadWriter: ", - ]); - + this.emitLine("},"); + this.emitLine("{"); this.indent(() => { - this.forEachEnumCase(e, "none", (_, jsonName) => { - let strBuild = ""; - const backticks = - shouldAddBacktick(jsonName) || - jsonName.includes(" ") || - !Number.isNaN(Number.parseInt(jsonName.charAt(0))); - this.emitItem(["case "]); - if (backticks) { - strBuild = strBuild + "`"; - } - - strBuild = strBuild + jsonName; - if (backticks) { - strBuild = strBuild + "`"; - } - - // if (--count > 0) strBuild + ","; - this.emitLine([strBuild]); + this.forEachEnumCase(e, "none", (name, jsonName) => { + this.emitLine([ + `case "${stringEscape(jsonName)}" => `, + enumName, + ".", + name, + ]); }); + this.emitLine([ + 'case other => throw new upickle.core.Abort("invalid ', + enumName, + ': " + other)', + ]); }); - } + this.emitLine("}"); + }); + this.emitLine(")"); + this.ensureBlankLine(); } } From 1f6a642f0c7488aa63ddbce890c50f264882643c Mon Sep 17 00:00:00 2001 From: Mark Probst Date: Sun, 19 Jul 2026 21:00:06 -0400 Subject: [PATCH 27/31] Register the scala3-upickle fixture and overhaul the scala3 skip lists PR #2246 added test/fixtures/scala3-upickle/ but never registered it: there was no Language entry, no fixture registration, and no CI matrix entry, so the upickle renderer had zero test exposure. This adds Scala3UpickleLanguage (framework: upickle), registers scala3-upickle / schema-scala3-upickle fixtures, and puts them in the CI matrix (the existing scala3 toolchain-setup conditions match the new names). Driver fixes: - circe.scala now exits nonzero on parse/decode failure (it used to print the DecodingFailure and exit 0, making every expected-failure schema sample undetectable), uses scala.Right/scala.Left patterns so generated types named Right/Left can't shadow them, and drops dead commented-out code. - upickle.scala drops the unused os-lib dependency and dead code. Skip lists are rebuilt from an exhaustive local generate+compile+ round-trip sweep of priority/samples/misc JSON and the schema corpus with scala-cli (Scala 3.2.2, circe 0.14.5, upickle 3.1.0): - Both frameworks now run everything except six inputs that hit the raw-identifier limitation (empty property names, bare "_", backticks/emoji in names, generated classes shadowing None/Option) plus keyword-unions.schema for the same reason. - Un-skipped for circe: github-events, kitchen-sink, no-classes, name-style, combinations1-4, unions, php-mixed-union, the whole "sorted differently" group, and 13 schemas including keyword-enum.schema, enum.schema, required.schema, intersection and the untagged-union group (the driver exit-code fix makes their fail samples detectable, and the renderer fixes make them pass). - Four misc inputs move from skipJSON to skipDiffViaSchema: they round-trip fine and only hit a pre-existing property-ordering quirk in the JSON-Schema-derived generation path. Co-Authored-By: Claude Fable 5 --- .github/workflows/test-pr.yaml | 1 + test/fixtures.ts | 5 + test/fixtures/scala3-upickle/.gitignore | 1 - test/fixtures/scala3-upickle/upickle.scala | 14 +-- test/fixtures/scala3/circe.scala | 41 +++----- test/languages.ts | 116 +++++++++++---------- 6 files changed, 89 insertions(+), 89 deletions(-) diff --git a/.github/workflows/test-pr.yaml b/.github/workflows/test-pr.yaml index 0b1c562522..ca58770bf3 100644 --- a/.github/workflows/test-pr.yaml +++ b/.github/workflows/test-pr.yaml @@ -53,6 +53,7 @@ jobs: - ruby - php,schema-php - scala3,schema-scala3 + - scala3-upickle,schema-scala3-upickle - elixir,schema-elixir,graphql-elixir - comment-injection-treesitter,comment-injection-typescript,comment-injection-typescript-zod,comment-injection-typescript-effect-schema diff --git a/test/fixtures.ts b/test/fixtures.ts index a49aef802f..6d22a9ac7c 100644 --- a/test/fixtures.ts +++ b/test/fixtures.ts @@ -1566,6 +1566,7 @@ export const allFixtures: Fixture[] = [ new JSONFixture(languages.JavaScriptLanguage), new JSONFixture(languages.KotlinLanguage), new JSONFixture(languages.Scala3Language), + new JSONFixture(languages.Scala3UpickleLanguage, "scala3-upickle"), new JSONFixture(languages.KotlinJacksonLanguage, "kotlin-jackson"), new JSONFixture(languages.KotlinXLanguage, "kotlinx"), new JSONFixture(languages.DartLanguage), @@ -1609,6 +1610,10 @@ export const allFixtures: Fixture[] = [ ), new JSONSchemaFixture(languages.KotlinXLanguage, "schema-kotlinx"), new JSONSchemaFixture(languages.Scala3Language), + new JSONSchemaFixture( + languages.Scala3UpickleLanguage, + "schema-scala3-upickle", + ), new JSONSchemaFixture(languages.DartLanguage), new JSONSchemaFixture(languages.PikeLanguage), new JSONSchemaFixture(languages.HaskellLanguage), diff --git a/test/fixtures/scala3-upickle/.gitignore b/test/fixtures/scala3-upickle/.gitignore index 03df7b5603..f252b419dc 100644 --- a/test/fixtures/scala3-upickle/.gitignore +++ b/test/fixtures/scala3-upickle/.gitignore @@ -1,4 +1,3 @@ main.jar .bsp .scala-build -rename/ \ No newline at end of file diff --git a/test/fixtures/scala3-upickle/upickle.scala b/test/fixtures/scala3-upickle/upickle.scala index 5071588087..d3e72865a5 100644 --- a/test/fixtures/scala3-upickle/upickle.scala +++ b/test/fixtures/scala3-upickle/upickle.scala @@ -1,17 +1,13 @@ //> using scala "3.2.2" -//> using lib "com.lihaoyi::upickle:3.1.0" -//> using lib "com.lihaoyi::os-lib:0.9.1" +//> using dep "com.lihaoyi::upickle:3.1.0" //> using options "-Xmax-inlines", "500000" package quicktype -import upickle.default.* - -@main def main = { - val json = scala.io.Source.fromFile("sample.json").getLines.mkString +@main def main = { + val json = scala.io.Source.fromFile("sample.json").getLines.mkString val parsed = OptionPickler.read[TopLevel](json) val jsonString = OptionPickler.writeJs(parsed) - val arr : Array[Byte] = jsonString.toString.getBytes("UTF-8") - // os.write(os.pwd / "my.json", OptionPickler.write(parsed, 2)) - System.out.write(arr, 0, arr.length) + val arr: Array[Byte] = jsonString.toString.getBytes("UTF-8") + System.out.write(arr, 0, arr.length) } diff --git a/test/fixtures/scala3/circe.scala b/test/fixtures/scala3/circe.scala index 318be045bc..a8e4b49867 100644 --- a/test/fixtures/scala3/circe.scala +++ b/test/fixtures/scala3/circe.scala @@ -1,34 +1,23 @@ //> using scala "3.2.2" -//> using lib "io.circe::circe-core:0.14.5" -//> using lib "io.circe::circe-parser:0.14.5" +//> using dep "io.circe::circe-core:0.14.5" +//> using dep "io.circe::circe-parser:0.14.5" //> using options "-Xmax-inlines", "3000" + package quicktype import io.circe._ import io.circe.parser._ import io.circe.syntax._ -import scala.util.Try -import scala.util.Right -import scala.util.Left - -/* - -case class TopLevel ( - val data : Long, - val next : Option[TopLevel] = None -) derives Encoder.AsObject, Decoder */ - -@main def main = - val json = scala.io.Source.fromFile("sample.json").getLines.mkString - - parse(json).map(x => - val arg = x.as[TopLevel] - arg match { - case Right(y) => - val jsonString = y.asJson - val arr : Array[Byte] = jsonString.toString.getBytes("UTF-8") - System.out.write(arr, 0, arr.length) - case Left(y) => println(y); println("-----");println(y.getMessage) - } - ) +@main def main = { + val json = scala.io.Source.fromFile("sample.json").getLines.mkString + // `scala.` in case the generated code has types named Right/Left. + parse(json).flatMap(_.as[TopLevel]) match { + case scala.Right(y) => + val arr: Array[Byte] = y.asJson.toString.getBytes("UTF-8") + System.out.write(arr, 0, arr.length) + case scala.Left(err) => + System.err.println(err) + sys.exit(1) + } +} diff --git a/test/languages.ts b/test/languages.ts index 4164af7c77..ec000aaac4 100644 --- a/test/languages.ts +++ b/test/languages.ts @@ -1044,57 +1044,81 @@ export const Scala3Language: Language = { return `cp "${sample}" sample.json && ./run.sh`; }, diffViaSchema: true, - skipDiffViaSchema: ["bug427.json", "keywords.json"], + skipDiffViaSchema: [ + "bug427.json", + // These round-trip fine; the code generated via JSON Schema + // orders one property differently (a pre-existing + // alphabetization quirk around renamed keyword properties). + "github-events.json", + "0a91a.json", + "34702.json", + "76ae1.json", + "af2d1.json", + ], allowMissingNull: true, features: ["enum", "union", "no-defaults"], output: "TopLevel.scala", topLevel: "TopLevel", skipJSON: [ - // These tests have "_" as a param name. Scala can't do this? + // The renderer emits raw JSON property names as (backticked) + // Scala identifiers, so empty names, a bare "_", and names + // containing backticks or line separators cannot compile, and + // properties named "None"/"Option" generate case classes that + // shadow the Scala prelude. "blns-object.json", "identifiers.json", "simple-identifiers.json", "keywords.json", + "nst-test-suite.json", - // these actually work as far as I can tell, but seem to fail because properties are sorted differently - // I don't think they fail... but I can't figure out sorting so hey ho let's skip them + // Scala3 has the same prelude-shadowing bug that this input + // guards against in Rust (issue #2521): a field named "options" + // generates `case class Option`, which shadows scala.Option. + "bug2521.json", + ], + skipSchema: [ + // Same raw-identifier limitation as in skipJSON: a property + // named "_" and classes shadowing None/Option don't compile. + "keyword-unions.schema", + ], + skipMiscJSON: false, + rendererOptions: { framework: "circe" }, + quickTestRendererOptions: [], + sourceFiles: ["src/language/Scala3/index.ts"], +}; + +export const Scala3UpickleLanguage: Language = { + name: "scala3", + base: "test/fixtures/scala3-upickle", + runCommand(sample: string) { + return `cp "${sample}" sample.json && ./run.sh`; + }, + diffViaSchema: true, + skipDiffViaSchema: [ + "bug427.json", + // These round-trip fine; the code generated via JSON Schema + // orders one property differently (a pre-existing + // alphabetization quirk around renamed keyword properties). "github-events.json", - "0a358.json", "0a91a.json", "34702.json", "76ae1.json", "af2d1.json", - "bug427.json", - "3d04a0.json", - - // Top level primitives... trivial, - // but annoying as it breaks compilation of the "Top Level" construct... which doesn't exist. - // It's too much hassle to fix - // and has no practical application in this context. Skip. - "no-classes.json", - - // spaces in variables names doesn't seem to work - "name-style.json", - - /* -I havea no idea how to encode these tests correctly. -*/ - "kitchen-sink.json", - "26c9c.json", - "421d4.json", - "a0496.json", - "fcca3.json", - "ae9ca.json", - "617e8.json", - "5f7fe.json", - "f74d5.json", - "a3d8c.json", - "combinations1.json", - "combinations2.json", - "combinations3.json", - "combinations4.json", - "unions.json", - "php-mixed-union.json", + ], + allowMissingNull: true, + features: ["enum", "union", "no-defaults"], + output: "TopLevel.scala", + topLevel: "TopLevel", + skipJSON: [ + // The renderer emits raw JSON property names as (backticked) + // Scala identifiers, so empty names, a bare "_", and names + // containing backticks or line separators cannot compile, and + // properties named "None"/"Option" generate case classes that + // shadow the Scala prelude. + "blns-object.json", + "identifiers.json", + "simple-identifiers.json", + "keywords.json", "nst-test-suite.json", // Scala3 has the same prelude-shadowing bug that this input @@ -1103,26 +1127,12 @@ I havea no idea how to encode these tests correctly. "bug2521.json", ], skipSchema: [ - // 12 skips - "required.schema", - "multi-type-enum.schema", // I think it doesn't correctly realise this is an array of enums. - "integer-string.schema", - "intersection.schema", - ...skipsUntypedUnions, - // The test driver prints the circe DecodingFailure and exits 0, so - // expected-failure samples cannot be detected. - "nested-intersection-union.schema", - "prefix-items.schema", - "date-time-or-string.schema", - "implicit-one-of.schema", - "go-schema-pattern-properties.schema", - ...skipsEnumValueValidation, - "class-with-additional.schema", + // Same raw-identifier limitation as in skipJSON: a property + // named "_" and classes shadowing None/Option don't compile. "keyword-unions.schema", - "keyword-enum.schema", ], skipMiscJSON: false, - rendererOptions: { framework: "circe" }, + rendererOptions: { framework: "upickle" }, quickTestRendererOptions: [], sourceFiles: ["src/language/Scala3/index.ts"], }; From 86dc02e53c591704fdf6dffb4e3e733fb6610654 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 20 Jul 2026 04:04:56 +0000 Subject: [PATCH 28/31] build(deps-dev): bump @types/unicode-properties from 1.3.0 to 1.3.2 Bumps [@types/unicode-properties](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/unicode-properties) from 1.3.0 to 1.3.2. - [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases) - [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/unicode-properties) --- updated-dependencies: - dependency-name: "@types/unicode-properties" dependency-version: 1.3.2 dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- package-lock.json | 6 ++++-- packages/quicktype-core/package.json | 2 +- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 3779d165a4..1c0d2569b6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2041,7 +2041,9 @@ } }, "node_modules/@types/unicode-properties": { - "version": "1.3.0", + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@types/unicode-properties/-/unicode-properties-1.3.2.tgz", + "integrity": "sha512-1gq48yvPn+sdJqG4tARHoQbVYyFCrV92gZdl2100vcP9n/u4nGIeLnxYyrTLWHQRhJYpc/w4SNDXUrKTjvEfRA==", "dev": true, "license": "MIT" }, @@ -10333,7 +10335,7 @@ "@types/is-url": "^1.2.32", "@types/node": "~20.19.0", "@types/pluralize": "0.0.30", - "@types/unicode-properties": "^1.3.0", + "@types/unicode-properties": "^1.3.2", "@types/wordwrap": "^1.0.3", "typescript": "~7.0.2" }, diff --git a/packages/quicktype-core/package.json b/packages/quicktype-core/package.json index b57811058d..ddd4e29c9e 100644 --- a/packages/quicktype-core/package.json +++ b/packages/quicktype-core/package.json @@ -49,7 +49,7 @@ "@types/is-url": "^1.2.32", "@types/node": "~20.19.0", "@types/pluralize": "0.0.30", - "@types/unicode-properties": "^1.3.0", + "@types/unicode-properties": "^1.3.2", "@types/wordwrap": "^1.0.3", "typescript": "~7.0.2" }, From c5775a3b0e432c3f4858d2e4d910c03d4dad2949 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 20 Jul 2026 04:05:06 +0000 Subject: [PATCH 29/31] build(deps): bump @types/urijs from 1.19.25 to 1.19.26 Bumps [@types/urijs](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/urijs) from 1.19.25 to 1.19.26. - [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases) - [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/urijs) --- updated-dependencies: - dependency-name: "@types/urijs" dependency-version: 1.19.26 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- package-lock.json | 8 +++++--- package.json | 2 +- packages/quicktype-core/package.json | 2 +- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index 3779d165a4..a9385b76fa 100644 --- a/package-lock.json +++ b/package-lock.json @@ -45,7 +45,7 @@ "@types/semver": "^7.5.0", "@types/shelljs": "^0.10.0", "@types/stream-json": "^1.7.3", - "@types/urijs": "^1.19.25", + "@types/urijs": "^1.19.26", "@types/wordwrap": "^1.0.3", "ajv": "^5.5.2", "deep-equal": "^2.2.3", @@ -2046,7 +2046,9 @@ "license": "MIT" }, "node_modules/@types/urijs": { - "version": "1.19.25", + "version": "1.19.26", + "resolved": "https://registry.npmjs.org/@types/urijs/-/urijs-1.19.26.tgz", + "integrity": "sha512-wkXrVzX5yoqLnndOwFsieJA7oKM8cNkOKJtf/3vVGSUFkWDKZvFHpIl9Pvqb/T9UsawBBFMTTD8xu7sK5MWuvg==", "license": "MIT" }, "node_modules/@types/vscode": { @@ -10316,7 +10318,7 @@ "dependencies": { "@glideapps/ts-necessities": "2.2.3", "@types/readable-stream": "4.0.10", - "@types/urijs": "^1.19.25", + "@types/urijs": "^1.19.26", "browser-or-node": "^3.0.0", "collection-utils": "^1.0.1", "is-url": "^1.2.4", diff --git a/package.json b/package.json index 09df9da7a1..f6b9361260 100644 --- a/package.json +++ b/package.json @@ -62,7 +62,7 @@ "@types/semver": "^7.5.0", "@types/shelljs": "^0.10.0", "@types/stream-json": "^1.7.3", - "@types/urijs": "^1.19.25", + "@types/urijs": "^1.19.26", "@types/wordwrap": "^1.0.3", "ajv": "^5.5.2", "deep-equal": "^2.2.3", diff --git a/packages/quicktype-core/package.json b/packages/quicktype-core/package.json index b57811058d..698e6da877 100644 --- a/packages/quicktype-core/package.json +++ b/packages/quicktype-core/package.json @@ -32,7 +32,7 @@ "dependencies": { "@glideapps/ts-necessities": "2.2.3", "@types/readable-stream": "4.0.10", - "@types/urijs": "^1.19.25", + "@types/urijs": "^1.19.26", "browser-or-node": "^3.0.0", "collection-utils": "^1.0.1", "is-url": "^1.2.4", From cc3c3b02d7213770f77d93a8f2fed4706f5c3de2 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 20 Jul 2026 12:21:38 +0000 Subject: [PATCH 30/31] build(deps-dev): bump ajv from 5.5.2 to 8.20.0 Bumps [ajv](https://github.com/ajv-validator/ajv) from 5.5.2 to 8.20.0. - [Release notes](https://github.com/ajv-validator/ajv/releases) - [Commits](https://github.com/ajv-validator/ajv/compare/v5.5.2...v8.20.0) --- updated-dependencies: - dependency-name: ajv dependency-version: 8.20.0 dependency-type: direct:development update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- package-lock.json | 102 ++++++++-------------------------------------- package.json | 2 +- 2 files changed, 19 insertions(+), 85 deletions(-) diff --git a/package-lock.json b/package-lock.json index bc94a988e2..649f32b828 100644 --- a/package-lock.json +++ b/package-lock.json @@ -47,7 +47,7 @@ "@types/stream-json": "^1.7.3", "@types/urijs": "^1.19.26", "@types/wordwrap": "^1.0.3", - "ajv": "^5.5.2", + "ajv": "^8.20.0", "deep-equal": "^2.2.3", "esbuild": "^0.28.1", "promise-timeout": "^1.3.0", @@ -1444,23 +1444,6 @@ "node": ">=20.0.0" } }, - "node_modules/@secretlint/config-loader/node_modules/ajv": { - "version": "8.20.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", - "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", - "dev": true, - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.3", - "fast-uri": "^3.0.1", - "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, "node_modules/@secretlint/config-loader/node_modules/debug": { "version": "4.4.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", @@ -1479,20 +1462,6 @@ } } }, - "node_modules/@secretlint/config-loader/node_modules/fast-deep-equal": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "dev": true, - "license": "MIT" - }, - "node_modules/@secretlint/config-loader/node_modules/json-schema-traverse": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", - "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", - "dev": true, - "license": "MIT" - }, "node_modules/@secretlint/config-loader/node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -2973,14 +2942,20 @@ } }, "node_modules/ajv": { - "version": "5.5.2", + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", + "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", "dev": true, "license": "MIT", "dependencies": { - "co": "^4.6.0", - "fast-deep-equal": "^1.0.0", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.3.0" + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" } }, "node_modules/ansi-colors": { @@ -3637,15 +3612,6 @@ "node": ">=12" } }, - "node_modules/co": { - "version": "4.6.0", - "dev": true, - "license": "MIT", - "engines": { - "iojs": ">= 1.0.0", - "node": ">= 0.12.0" - } - }, "node_modules/cockatiel": { "version": "3.1.2", "dev": true, @@ -4614,7 +4580,9 @@ "license": "MIT" }, "node_modules/fast-deep-equal": { - "version": "1.1.0", + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", "dev": true, "license": "MIT" }, @@ -4634,11 +4602,6 @@ "node": ">=8.6.0" } }, - "node_modules/fast-json-stable-stringify": { - "version": "2.0.0", - "dev": true, - "license": "MIT" - }, "node_modules/fast-levenshtein": { "version": "2.0.6", "dev": true, @@ -5630,7 +5593,9 @@ } }, "node_modules/json-schema-traverse": { - "version": "0.3.1", + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", "dev": true, "license": "MIT" }, @@ -8602,37 +8567,6 @@ "node": ">=12.17" } }, - "node_modules/table/node_modules/ajv": { - "version": "8.20.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", - "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", - "dev": true, - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.3", - "fast-uri": "^3.0.1", - "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/table/node_modules/fast-deep-equal": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "dev": true, - "license": "MIT" - }, - "node_modules/table/node_modules/json-schema-traverse": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", - "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", - "dev": true, - "license": "MIT" - }, "node_modules/tapable": { "version": "2.2.1", "dev": true, diff --git a/package.json b/package.json index f6b9361260..e02446cc76 100644 --- a/package.json +++ b/package.json @@ -64,7 +64,7 @@ "@types/stream-json": "^1.7.3", "@types/urijs": "^1.19.26", "@types/wordwrap": "^1.0.3", - "ajv": "^5.5.2", + "ajv": "^8.20.0", "deep-equal": "^2.2.3", "esbuild": "^0.28.1", "promise-timeout": "^1.3.0", From 608516ae14dfa07181a48f1ba47f8b124855e071 Mon Sep 17 00:00:00 2001 From: Mark Probst Date: Mon, 20 Jul 2026 08:29:37 -0400 Subject: [PATCH 31/31] test: adapt schema-json fixture to Ajv 8 API Ajv 8 removed the `format` and `unknownFormats` constructor options and no longer bundles the draft-06 meta-schema that our generated schemas declare. Update the JSONSchemaJSONFixture validation accordingly: - register the draft-06 meta-schema shipped in ajv/dist/refs - install format validators from the new ajv-formats package (its default "full" mode matches the old `format: "full"` option); our custom date-time format is registered afterwards so it still wins - declare the quicktype-specific schema keywords qt-uri-protocols and qt-uri-extensions as a vocabulary so strict mode accepts them - register the "integer" and "boolean" pseudo-formats as always-valid (`true`), replacing `unknownFormats: ["integer", "boolean"]` Verified with QUICKTEST=true FIXTURE=schema-json-csharp script/test (45/45 passing) and the unit test suite. Co-Authored-By: Claude Fable 5 --- package-lock.json | 19 +++++++++++++++++++ package.json | 1 + test/fixtures.ts | 21 ++++++++++++++++----- 3 files changed, 36 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index 649f32b828..0436aecd16 100644 --- a/package-lock.json +++ b/package-lock.json @@ -48,6 +48,7 @@ "@types/urijs": "^1.19.26", "@types/wordwrap": "^1.0.3", "ajv": "^8.20.0", + "ajv-formats": "^3.0.1", "deep-equal": "^2.2.3", "esbuild": "^0.28.1", "promise-timeout": "^1.3.0", @@ -2958,6 +2959,24 @@ "url": "https://github.com/sponsors/epoberezkin" } }, + "node_modules/ajv-formats": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz", + "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, "node_modules/ansi-colors": { "version": "4.1.1", "dev": true, diff --git a/package.json b/package.json index e02446cc76..9324aeb5e9 100644 --- a/package.json +++ b/package.json @@ -65,6 +65,7 @@ "@types/urijs": "^1.19.26", "@types/wordwrap": "^1.0.3", "ajv": "^8.20.0", + "ajv-formats": "^3.0.1", "deep-equal": "^2.2.3", "esbuild": "^0.28.1", "promise-timeout": "^1.3.0", diff --git a/test/fixtures.ts b/test/fixtures.ts index 6d22a9ac7c..dece181ef6 100644 --- a/test/fixtures.ts +++ b/test/fixtures.ts @@ -5,6 +5,8 @@ import { randomBytes } from "node:crypto"; import * as shell from "shelljs"; const Ajv = require("ajv"); +const addFormats = require("ajv-formats"); +const draft06MetaSchema = require("ajv/dist/refs/json-schema-draft-06.json"); import { compareJsonFileToJson, @@ -600,17 +602,26 @@ class JSONSchemaJSONFixture extends JSONToXToYFixture { fs.readFileSync(this.language.output, "utf8"), ); - const ajv = new Ajv({ - format: "full", - unknownFormats: ["integer", "boolean"], - }); + const ajv = new Ajv(); + // We generate draft-06 schemas, which Ajv 8 doesn't support out of + // the box anymore. + ajv.addMetaSchema(draft06MetaSchema); + // Ajv 8 moved the format validators into the ajv-formats package; + // its default mode is "full", like the old `format: "full"` option. + addFormats(ajv); + // Our custom schema keywords, which strict mode would reject. + ajv.addVocabulary(["qt-uri-protocols", "qt-uri-extensions"]); // Make Ajv's date-time compatible with what we recognize. All non-standard // JSON formats that we use for transformed type kinds must be registered here - // with a validation function. + // with a validation function. Formats registered with `true` are + // accepted without validating the string. This replaces the old + // `unknownFormats: ["integer", "boolean"]` option. // FIXME: Unify this with what's in StringTypes.ts. ajv.addFormat("date-time", (s: string) => dateTimeRecognizer.isDateTime(s), ); + ajv.addFormat("integer", true); + ajv.addFormat("boolean", true); const valid = ajv.validate(schema, input); if (!valid) { failWith("Generated schema does not validate input JSON.", {