From 3afe6051fe68eb78a75b9de1c6fadf21877e7a9a Mon Sep 17 00:00:00 2001 From: Mark Probst Date: Mon, 7 Sep 2026 18:04:31 -0700 Subject: [PATCH 1/3] fix(java): reject invalid calendar dates --- .../src/language/Java/DateTimeProvider.ts | 12 +++++++++--- .../quicktype-core/src/language/Java/JavaRenderer.ts | 3 +++ 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/packages/quicktype-core/src/language/Java/DateTimeProvider.ts b/packages/quicktype-core/src/language/Java/DateTimeProvider.ts index 2535fcdfac..6298f885a3 100644 --- a/packages/quicktype-core/src/language/Java/DateTimeProvider.ts +++ b/packages/quicktype-core/src/language/Java/DateTimeProvider.ts @@ -6,6 +6,7 @@ export abstract class JavaDateTimeProvider { public constructor( protected readonly _renderer: JavaRenderer, protected readonly _className: string, + protected readonly _useStrictDateTime = false, ) {} public abstract keywords: string[]; @@ -105,15 +106,20 @@ export class Java8DateTimeProvider extends JavaDateTimeProvider { ".appendOptional(DateTimeFormatter.ISO_INSTANT)", ); this._renderer.emitLine( - '.appendOptional(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SX"))', + '.appendOptional(DateTimeFormatter.ofPattern("uuuu-MM-dd HH:mm:ss.SX"))', ); this._renderer.emitLine( - '.appendOptional(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ssX"))', + '.appendOptional(DateTimeFormatter.ofPattern("uuuu-MM-dd HH:mm:ssX"))', ); this._renderer.emitLine( - '.appendOptional(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"))', + '.appendOptional(DateTimeFormatter.ofPattern("uuuu-MM-dd HH:mm:ss"))', ); this._renderer.emitLine(".toFormatter()"); + if (this._useStrictDateTime) { + this._renderer.emitLine( + ".withResolverStyle(java.time.format.ResolverStyle.STRICT)", + ); + } this._renderer.emitLine(".withZone(ZoneOffset.UTC);"); }), ); diff --git a/packages/quicktype-core/src/language/Java/JavaRenderer.ts b/packages/quicktype-core/src/language/Java/JavaRenderer.ts index 3817dcc9f0..a18ad85563 100644 --- a/packages/quicktype-core/src/language/Java/JavaRenderer.ts +++ b/packages/quicktype-core/src/language/Java/JavaRenderer.ts @@ -78,6 +78,9 @@ export class JavaRenderer extends ConvenienceRenderer { this._dateTimeProvider = new Java8DateTimeProvider( this, this._converterClassname, + [...renderContext.typeGraph.allTypesUnordered()].some( + (t) => t.kind === "date-time", + ), ); break; } From e86a06c49cb5a8f2fa8f9c3db5a709be9d980b87 Mon Sep 17 00:00:00 2001 From: Mark Probst Date: Mon, 7 Sep 2026 10:51:31 -0700 Subject: [PATCH 2/3] fix(swift): reject invalid calendar dates --- .../src/language/Swift/SwiftRenderer.ts | 15 +++++++++++++++ .../schema/date-time.11.fail.date-time.json | 7 +++++++ .../inputs/schema/date-time.9.fail.date-time.json | 7 +++++++ 3 files changed, 29 insertions(+) create mode 100644 test/inputs/schema/date-time.11.fail.date-time.json create mode 100644 test/inputs/schema/date-time.9.fail.date-time.json diff --git a/packages/quicktype-core/src/language/Swift/SwiftRenderer.ts b/packages/quicktype-core/src/language/Swift/SwiftRenderer.ts index 4963e2aaff..49bf872c63 100644 --- a/packages/quicktype-core/src/language/Swift/SwiftRenderer.ts +++ b/packages/quicktype-core/src/language/Swift/SwiftRenderer.ts @@ -743,6 +743,15 @@ export class SwiftRenderer extends ConvenienceRenderer { } private emitNewEncoderDecoder(): void { + this.emitMultiline(`private func isValidDate(_ value: String) -> Bool { + let formatter = DateFormatter() + formatter.locale = Locale(identifier: "en_US_POSIX") + formatter.timeZone = TimeZone(secondsFromGMT: 0) + formatter.dateFormat = "u-MM-dd" + formatter.isLenient = false + return formatter.date(from: String(value.prefix(10))) != nil +}`); + this.ensureBlankLine(); this.emitBlock("func newJSONDecoder() -> JSONDecoder", () => { this.emitLine("let decoder = JSONDecoder()"); if (!this._options.linux) { @@ -751,6 +760,9 @@ export class SwiftRenderer extends ConvenienceRenderer { () => { this.emitMultiline(`decoder.dateDecodingStrategy = .custom { decoder in let dateStr = try decoder.singleValueContainer().decode(String.self).uppercased() + guard isValidDate(dateStr) else { + throw DecodingError.dataCorrupted(.init(codingPath: decoder.codingPath, debugDescription: "Invalid date")) + } let dateData = try JSONEncoder().encode(dateStr) let dateDecoder = JSONDecoder() dateDecoder.dateDecodingStrategy = .iso8601 @@ -762,6 +774,9 @@ export class SwiftRenderer extends ConvenienceRenderer { this.emitMultiline(`decoder.dateDecodingStrategy = .custom({ (decoder) -> Date in let container = try decoder.singleValueContainer() let dateStr = try container.decode(String.self).uppercased() + guard isValidDate(dateStr) else { + throw DecodingError.dataCorrupted(.init(codingPath: decoder.codingPath, debugDescription: "Invalid date")) + } let formatter = DateFormatter() formatter.calendar = Calendar(identifier: .iso8601) diff --git a/test/inputs/schema/date-time.11.fail.date-time.json b/test/inputs/schema/date-time.11.fail.date-time.json new file mode 100644 index 0000000000..34582b9df5 --- /dev/null +++ b/test/inputs/schema/date-time.11.fail.date-time.json @@ -0,0 +1,7 @@ +{ + "date": "1985-04-12", + "time": "23:20:50.52Z", + "date-time": "1900-02-29T12:00:00Z", + "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/inputs/schema/date-time.9.fail.date-time.json b/test/inputs/schema/date-time.9.fail.date-time.json new file mode 100644 index 0000000000..9c07dc2bbb --- /dev/null +++ b/test/inputs/schema/date-time.9.fail.date-time.json @@ -0,0 +1,7 @@ +{ + "date": "1985-04-12", + "time": "23:20:50.52Z", + "date-time": "2023-02-29T12:00:00Z", + "union-array": ["1985-04-12", "23:20:50.52Z"], + "complex-union-array": ["2018-08-13T21:31:01+00:10", "foo", 123] +} From f46fd23daac41567a4ac9d794e22c2cd21dfd2fe Mon Sep 17 00:00:00 2001 From: Mark Probst Date: Mon, 7 Sep 2026 18:05:00 -0700 Subject: [PATCH 3/3] fix(swift): scope calendar validation to date-time output --- .../src/language/Swift/SwiftRenderer.ts | 25 +++++++++++-------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/packages/quicktype-core/src/language/Swift/SwiftRenderer.ts b/packages/quicktype-core/src/language/Swift/SwiftRenderer.ts index 49bf872c63..7130022215 100644 --- a/packages/quicktype-core/src/language/Swift/SwiftRenderer.ts +++ b/packages/quicktype-core/src/language/Swift/SwiftRenderer.ts @@ -48,6 +48,8 @@ export class SwiftRenderer extends ConvenienceRenderer { private _needNull = false; + private _haveDateTime = false; + public constructor( targetLanguage: TargetLanguage, renderContext: RenderContext, @@ -215,6 +217,7 @@ export class SwiftRenderer extends ConvenienceRenderer { }, (transformedStringType) => { if (transformedStringType.kind === "date-time") { + this._haveDateTime = true; return "Date"; } if (transformedStringType.kind === "uuid") return "UUID"; @@ -743,7 +746,8 @@ export class SwiftRenderer extends ConvenienceRenderer { } private emitNewEncoderDecoder(): void { - this.emitMultiline(`private func isValidDate(_ value: String) -> Bool { + if (this._haveDateTime) { + this.emitMultiline(`private func isValidDate(_ value: String) -> Bool { let formatter = DateFormatter() formatter.locale = Locale(identifier: "en_US_POSIX") formatter.timeZone = TimeZone(secondsFromGMT: 0) @@ -751,7 +755,14 @@ export class SwiftRenderer extends ConvenienceRenderer { formatter.isLenient = false return formatter.date(from: String(value.prefix(10))) != nil }`); - this.ensureBlankLine(); + this.ensureBlankLine(); + } + const dateValidation = this._haveDateTime + ? ` + guard isValidDate(dateStr) else { + throw DecodingError.dataCorrupted(.init(codingPath: decoder.codingPath, debugDescription: "Invalid date")) + }` + : ""; this.emitBlock("func newJSONDecoder() -> JSONDecoder", () => { this.emitLine("let decoder = JSONDecoder()"); if (!this._options.linux) { @@ -759,10 +770,7 @@ export class SwiftRenderer extends ConvenienceRenderer { "if #available(iOS 10.0, OSX 10.12, tvOS 10.0, watchOS 3.0, *)", () => { this.emitMultiline(`decoder.dateDecodingStrategy = .custom { decoder in - let dateStr = try decoder.singleValueContainer().decode(String.self).uppercased() - guard isValidDate(dateStr) else { - throw DecodingError.dataCorrupted(.init(codingPath: decoder.codingPath, debugDescription: "Invalid date")) - } + let dateStr = try decoder.singleValueContainer().decode(String.self).uppercased()${dateValidation} let dateData = try JSONEncoder().encode(dateStr) let dateDecoder = JSONDecoder() dateDecoder.dateDecodingStrategy = .iso8601 @@ -773,10 +781,7 @@ export class SwiftRenderer extends ConvenienceRenderer { } else { this.emitMultiline(`decoder.dateDecodingStrategy = .custom({ (decoder) -> Date in let container = try decoder.singleValueContainer() - let dateStr = try container.decode(String.self).uppercased() - guard isValidDate(dateStr) else { - throw DecodingError.dataCorrupted(.init(codingPath: decoder.codingPath, debugDescription: "Invalid date")) - } + let dateStr = try container.decode(String.self).uppercased()${dateValidation} let formatter = DateFormatter() formatter.calendar = Calendar(identifier: .iso8601)