diff --git a/packages/quicktype-core/src/language/JavaScript/JavaScriptRenderer.ts b/packages/quicktype-core/src/language/JavaScript/JavaScriptRenderer.ts index 63d5b5cd66..6186461959 100644 --- a/packages/quicktype-core/src/language/JavaScript/JavaScriptRenderer.ts +++ b/packages/quicktype-core/src/language/JavaScript/JavaScriptRenderer.ts @@ -445,10 +445,15 @@ ${hasArrayConstraints ? ' if ((typ.min !== undefined && val.length < typ. if (val === null) { return null; } + if (!(val instanceof Date) && (typeof val !== "string" || !/^[0-9]{4}-(?:0[1-9]|1[0-2])-(?:[0-2][0-9]|3[01])(?:T(?:[01][0-9]|2[0-3]):[0-5][0-9]:[0-5][0-9](?:[.][0-9]+)?(?:Z|[+-](?:[01][0-9]|2[0-3]):[0-5][0-9]))?$/i.test(val))) + return invalidValue(l("Date"), val, key, parent); const d = new Date(val); if (isNaN(d.valueOf())) { return invalidValue(l("Date"), val, key, parent); } + const date = typeof val === "string" ? val.slice(0, 10) : null; + if (date !== null && new Date(date + "T00:00:00Z").toISOString().slice(0, 10) !== date) + return invalidValue(l("Date"), val, key, parent); return d; } diff --git a/packages/quicktype-core/src/language/JavaScriptPropTypes/JavaScriptPropTypesRenderer.ts b/packages/quicktype-core/src/language/JavaScriptPropTypes/JavaScriptPropTypesRenderer.ts index 17b329a2d5..d03969986c 100644 --- a/packages/quicktype-core/src/language/JavaScriptPropTypes/JavaScriptPropTypesRenderer.ts +++ b/packages/quicktype-core/src/language/JavaScriptPropTypes/JavaScriptPropTypesRenderer.ts @@ -209,7 +209,7 @@ export class JavaScriptPropTypesRenderer extends ConvenienceRenderer { return '(props, name) => props[name] == null || /^[0-9a-f]{8}-(?:[0-9a-f]{4}-){3}[0-9a-f]{12}$/i.test(props[name]) ? null : new Error("Expected UUID")'; } if (transformedStringType.kind === "date-time") { - return '(props, name) => props[name] == null || typeof props[name] === "string" && !Number.isNaN(Date.parse(props[name])) ? null : new Error("Expected date-time")'; + return '(props, name) => props[name] == null || typeof props[name] === "string" && /^(\\d{4}-(?:0[1-9]|1[0-2])-(?:[0-2]\\d|3[01]))(?:T(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z|[+-](?:[01]\\d|2[0-3]):[0-5]\\d))?$/i.test(props[name]) && !Number.isNaN(Date.parse(`${props[name].slice(0, 10)}T00:00:00Z`)) && new Date(Date.parse(`${props[name].slice(0, 10)}T00:00:00Z`)).toISOString().slice(0, 10) === props[name].slice(0, 10) ? null : new Error("Expected date-time")'; } return "PropTypes.string"; }, diff --git a/test/inputs/schema/date-time.8.fail.date-time.json b/test/inputs/schema/date-time.8.fail.date-time.json new file mode 100644 index 0000000000..23a1d94a10 --- /dev/null +++ b/test/inputs/schema/date-time.8.fail.date-time.json @@ -0,0 +1,7 @@ +{ + "date": "1985-04-12", + "time": "23:20:50.52Z", + "date-time": "1", + "union-array": ["1985-04-12", "23:20:50.52Z"], + "complex-union-array": ["2018-08-13T21:31:01+00:10", "foo", 123] +} diff --git a/test/unit/javascript-date-api.test.ts b/test/unit/javascript-date-api.test.ts new file mode 100644 index 0000000000..f96c04e55d --- /dev/null +++ b/test/unit/javascript-date-api.test.ts @@ -0,0 +1,42 @@ +import { InputData, JSONSchemaInput, quicktype } from "quicktype-core"; +import { expect, test } from "vitest"; + +interface GeneratedConverters { + topLevelToJson: (value: { when: Date }) => string; +} + +async function converters(): Promise { + const schemaInput = new JSONSchemaInput(undefined); + await schemaInput.addSource({ + name: "TopLevel", + schema: JSON.stringify({ + type: "object", + properties: { + when: { type: "string", format: "date-time" }, + }, + required: ["when"], + }), + }); + const inputData = new InputData(); + inputData.addInput(schemaInput); + const result = await quicktype({ inputData, lang: "javascript" }); + const generatedModule: { exports: Partial } = { + exports: {}, + }; + new Function("exports", "module", result.lines.join("\n"))( + generatedModule.exports, + generatedModule, + ); + return generatedModule.exports as GeneratedConverters; +} + +test("JavaScript converter validates Date instances", async () => { + const { topLevelToJson } = await converters(); + + expect( + JSON.parse(topLevelToJson({ when: new Date("2024-02-29T00:00:00Z") })), + ).toEqual({ when: "2024-02-29T00:00:00.000Z" }); + expect(() => topLevelToJson({ when: new Date(Number.NaN) })).toThrow( + "Expected Date", + ); +});