diff --git a/packages/quicktype-core/src/language/Elm/ElmRenderer.ts b/packages/quicktype-core/src/language/Elm/ElmRenderer.ts index 22a0d92e83..240869dd07 100644 --- a/packages/quicktype-core/src/language/Elm/ElmRenderer.ts +++ b/packages/quicktype-core/src/language/Elm/ElmRenderer.ts @@ -67,6 +67,8 @@ export class ElmRenderer extends ConvenienceRenderer { } protected forbiddenNamesForGlobalNamespace(): readonly string[] { + if (this.forEachType((type) => type.kind).has("map")) + return [...forbiddenNames, "makeDictEncoder"]; return forbiddenNames; } @@ -344,7 +346,7 @@ export class ElmRenderer extends ConvenienceRenderer { (mapType) => multiWord( " ", - "Jenc.dict", + "makeDictEncoder", "identity", parenIfNeeded(this.encoderNameForType(mapType.values)), ), @@ -801,6 +803,16 @@ import Dict exposing (Dict)`); this.emitLine("--- encoder helpers"); this.ensureBlankLine(); + if (this.haveMaps) { + this.emitMultiline(`makeDictEncoder : (String -> String) -> (a -> Jenc.Value) -> Dict String a -> Jenc.Value +makeDictEncoder f m r = + r + |> Dict.toList + |> List.map (\\( x, y ) -> Jenc.encode 0 (Jenc.string (f x)) ++ ":" ++ Jenc.encode 0 (m y)) + |> String.join "," + |> (\\str -> Jdec.decodeString Jdec.value ("{" ++ str ++ "}") |> Result.withDefault Jenc.null)`); + this.ensureBlankLine(); + } this.emitMultiline(`makeNullableEncoder : (a -> Jenc.Value) -> Maybe a -> Jenc.Value makeNullableEncoder f m = case m of diff --git a/packages/quicktype-core/src/language/JavaScript/JavaScriptRenderer.ts b/packages/quicktype-core/src/language/JavaScript/JavaScriptRenderer.ts index 63d5b5cd66..570d967941 100644 --- a/packages/quicktype-core/src/language/JavaScript/JavaScriptRenderer.ts +++ b/packages/quicktype-core/src/language/JavaScript/JavaScriptRenderer.ts @@ -456,7 +456,7 @@ ${hasArrayConstraints ? ' if ((typ.min !== undefined && val.length < typ. if (val === null || typeof val !== "object" || Array.isArray(val)) { return invalidValue(l(ref || "object"), val, key, parent); } - const result${anyAnnotation} = {}; + const result${anyAnnotation} = Object.create(null); Object.getOwnPropertyNames(props).forEach(key => { const prop = props[key]; const v = Object.prototype.hasOwnProperty.call(val, key) ? val[key] : undefined; @@ -471,7 +471,7 @@ ${hasArrayConstraints ? ' if ((typ.min !== undefined && val.length < typ. }; } }); - return result; + return Object.setPrototypeOf(result, Object.prototype); } if (typ === "any") return val; diff --git a/packages/quicktype-core/src/language/TypeScriptEffectSchema/TypeScriptEffectSchemaRenderer.ts b/packages/quicktype-core/src/language/TypeScriptEffectSchema/TypeScriptEffectSchemaRenderer.ts index b61a3764b2..4df2c713f5 100644 --- a/packages/quicktype-core/src/language/TypeScriptEffectSchema/TypeScriptEffectSchemaRenderer.ts +++ b/packages/quicktype-core/src/language/TypeScriptEffectSchema/TypeScriptEffectSchemaRenderer.ts @@ -155,9 +155,9 @@ export class TypeScriptEffectSchemaRenderer extends ConvenienceRenderer { }, (_classType) => panic("Should already be handled."), (_mapType) => [ - "S.Record({ key: S.String, value: ", + "mapSchema(", this.typeMapTypeFor(_mapType.values, false), - "})", + ")", ], (_enumType) => panic("Should already be handled."), (unionType) => { @@ -406,6 +406,26 @@ export class TypeScriptEffectSchemaRenderer extends ConvenienceRenderer { } this.emitImports(); + if (this.haveMaps) { + this.emitMultiline(` +const objectSchema = () => + S.declare( + (input): input is Record => + typeof input === "object" && input !== null && !Array.isArray(input) + ); +const mapSchema = (value: S.Schema) => { + const entries = S.transform(objectSchema(), S.Array(S.Tuple(S.String, value)), { + strict: false, + decode: Object.entries, + encode: Object.fromEntries + }); + return S.transform(entries, objectSchema(), { + strict: false, + decode: Object.fromEntries, + encode: Object.entries + }); +};`); + } this.emitSchemas(); } } diff --git a/test/inputs/json/priority/keywords.json b/test/inputs/json/priority/keywords.json index bd1573fac7..672625c26b 100644 --- a/test/inputs/json/priority/keywords.json +++ b/test/inputs/json/priority/keywords.json @@ -186,6 +186,7 @@ "Locale": { "Locale": 123 }, "lock": { "lock": 123 }, "long": { "long": 123 }, + "makeDictEncoder": { "makeDictEncoder": 123 }, "map": { "map": 123 }, "MarshalJSON": { "MarshalJSON": 123 }, "MapEntry": { "MapEntry": 123 }, @@ -197,11 +198,11 @@ "native": { "native": 123 }, "new": { "new": 123 }, "newtonsoft": { "newtonsoft": 123 }, - "nil": { "nil": 123 }, - "NO": { "NO": 123 }, "dummy": 123 }, "obj4": { + "nil": { "nil": 123 }, + "NO": { "NO": 123 }, "noexcept": { "noexcept": 123 }, "nonatomic": { "nonatomic": 123 }, "none": { "none": 123 }, @@ -264,11 +265,11 @@ "runtimeType": { "runtimeType": 123 }, "s": { "s": 123 }, "sbyte": { "sbyte": 123 }, - "sealed": { "sealed": 123 }, - "SEL": { "SEL": 123 }, "dummy": 123 }, "obj5": { + "sealed": { "sealed": 123 }, + "SEL": { "SEL": 123 }, "select": { "select": 123 }, "Self": { "Self": 123 }, "Serializable": { "Serializable": 123 }, @@ -331,12 +332,12 @@ "undefined": { "undefined": 123 }, "union": { "union": 123 }, "UnmarshalJSON": { "UnmarshalJSON": 123 }, - "UseSerializers": { "UseSerializers": 123 }, - "unowned": { "unowned": 123 }, - "unsafe": { "unsafe": 123 }, "dummy": 123 }, "obj6": { + "UseSerializers": { "UseSerializers": 123 }, + "unowned": { "unowned": 123 }, + "unsafe": { "unsafe": 123 }, "unsigned": { "unsigned": 123 }, "ushort": { "ushort": 123 }, "using": { "using": 123 }, diff --git a/test/inputs/schema/class-map-union.5.json b/test/inputs/schema/class-map-union.5.json new file mode 100644 index 0000000000..489945ea17 --- /dev/null +++ b/test/inputs/schema/class-map-union.5.json @@ -0,0 +1,5 @@ +{ + "union": { + "__proto__": true + } +} diff --git a/test/inputs/schema/keyword-enum.schema b/test/inputs/schema/keyword-enum.schema index 01b8bc3559..127453aaaf 100644 --- a/test/inputs/schema/keyword-enum.schema +++ b/test/inputs/schema/keyword-enum.schema @@ -184,6 +184,7 @@ "Locale", "lock", "long", + "makeDictEncoder", "map", "MarshalJSON", "MapEntry", diff --git a/test/inputs/schema/keyword-unions.schema b/test/inputs/schema/keyword-unions.schema index 034a0eedfc..de53dccd37 100644 --- a/test/inputs/schema/keyword-unions.schema +++ b/test/inputs/schema/keyword-unions.schema @@ -1261,6 +1261,13 @@ ], "title": "union_long" }, + "makeDictEncoder": { + "oneOf": [ + { "type": "number" }, + { "type": "object", "additionalProperties": false, "title": "makeDictEncoder" } + ], + "title": "union_makeDictEncoder" + }, "map": { "oneOf": [ { "type": "number" }, diff --git a/test/keywords.txt b/test/keywords.txt index ff7c572a7c..f1b59336e9 100644 --- a/test/keywords.txt +++ b/test/keywords.txt @@ -178,6 +178,7 @@ list Locale lock long +makeDictEncoder map MarshalJSON MapEntry diff --git a/test/unit/javascript-prototype-key.test.ts b/test/unit/javascript-prototype-key.test.ts new file mode 100644 index 0000000000..41e93de7eb --- /dev/null +++ b/test/unit/javascript-prototype-key.test.ts @@ -0,0 +1,24 @@ +import vm from "node:vm"; + +import { InputData, JSONSchemaInput, quicktype } from "quicktype-core"; +import { expect, test } from "vitest"; + +test("JavaScript converters return plain objects for prototype-named keys", async () => { + const schemaInput = new JSONSchemaInput(undefined); + await schemaInput.addSource({ + name: "TopLevel", + schema: JSON.stringify({ + type: "object", + additionalProperties: { type: "boolean" }, + }), + }); + const inputData = new InputData(); + inputData.addInput(schemaInput); + const result = await quicktype({ inputData, lang: "javascript" }); + const module = { exports: {} as Record object> }; + + vm.runInNewContext(result.lines.join("\n"), { module, Object }); + const converted = module.exports.toTopLevel('{"__proto__":true}'); + + expect(Object.getPrototypeOf(converted)).toBe(Object.prototype); +});