From 22593d38c4ee08284c36d0d2564253ba4884136e Mon Sep 17 00:00:00 2001 From: Mark Probst Date: Mon, 7 Sep 2026 07:29:49 -0700 Subject: [PATCH 1/3] fix(swift): accept lowercase RFC 3339 dates --- .../src/language/Swift/SwiftRenderer.ts | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/packages/quicktype-core/src/language/Swift/SwiftRenderer.ts b/packages/quicktype-core/src/language/Swift/SwiftRenderer.ts index 6d22b46ed2..4963e2aaff 100644 --- a/packages/quicktype-core/src/language/Swift/SwiftRenderer.ts +++ b/packages/quicktype-core/src/language/Swift/SwiftRenderer.ts @@ -749,15 +749,19 @@ export class SwiftRenderer extends ConvenienceRenderer { this.emitBlock( "if #available(iOS 10.0, OSX 10.12, tvOS 10.0, watchOS 3.0, *)", () => { - this.emitLine( - "decoder.dateDecodingStrategy = .iso8601", - ); + this.emitMultiline(`decoder.dateDecodingStrategy = .custom { decoder in + let dateStr = try decoder.singleValueContainer().decode(String.self).uppercased() + let dateData = try JSONEncoder().encode(dateStr) + let dateDecoder = JSONDecoder() + dateDecoder.dateDecodingStrategy = .iso8601 + return try dateDecoder.decode(Date.self, from: dateData) +}`); }, ); } else { this.emitMultiline(`decoder.dateDecodingStrategy = .custom({ (decoder) -> Date in let container = try decoder.singleValueContainer() - let dateStr = try container.decode(String.self) + let dateStr = try container.decode(String.self).uppercased() let formatter = DateFormatter() formatter.calendar = Calendar(identifier: .iso8601) From a884773c5747b5cbec8873147dcce8bd96df8a02 Mon Sep 17 00:00:00 2001 From: Mark Probst Date: Mon, 7 Sep 2026 10:56:56 -0700 Subject: [PATCH 2/3] fix: validate native ISO date-times --- .../src/language/CPlusPlus/CPlusPlusRenderer.ts | 2 +- .../src/language/Objective-C/ObjectiveCRenderer.ts | 5 ++++- packages/quicktype-core/src/language/Php/PhpRenderer.ts | 8 ++++++++ 3 files changed, 13 insertions(+), 2 deletions(-) diff --git a/packages/quicktype-core/src/language/CPlusPlus/CPlusPlusRenderer.ts b/packages/quicktype-core/src/language/CPlusPlus/CPlusPlusRenderer.ts index ab8fce746e..5f51b96a09 100644 --- a/packages/quicktype-core/src/language/CPlusPlus/CPlusPlusRenderer.ts +++ b/packages/quicktype-core/src/language/CPlusPlus/CPlusPlusRenderer.ts @@ -1542,7 +1542,7 @@ export class CPlusPlusRenderer extends ConvenienceRenderer { key, ") != j.end() && !std::regex_match(j.at(", key, - ').get(), std::regex("^[0-9]{4}-[0-9]{2}-[0-9]{2}T.*$"))) throw std::runtime_error("Expected date-time");', + ').get(), std::regex("^(?:(?:[0-9]{4}-(?:0[13578]|1[02])-(?:0[1-9]|[12][0-9]|3[01]))|(?:[0-9]{4}-(?:0[469]|11)-(?:0[1-9]|[12][0-9]|30))|(?:[0-9]{4}-02-(?:0[1-9]|1[0-9]|2[0-8]))|(?:(?:[0-9]{2}(?:0[48]|[2468][048]|[13579][26])|(?:[02468][048]|[13579][26])00)-02-29))[Tt](?:[01][0-9]|2[0-3]):[0-5][0-9]:[0-5][0-9](?:[.][0-9]+)?(?:[Zz]|[+-](?:[01][0-9]|2[0-3]):[0-5][0-9])?$"))) throw std::runtime_error("Expected date-time");', ); } diff --git a/packages/quicktype-core/src/language/Objective-C/ObjectiveCRenderer.ts b/packages/quicktype-core/src/language/Objective-C/ObjectiveCRenderer.ts index eb970fda54..8d19dd50f5 100644 --- a/packages/quicktype-core/src/language/Objective-C/ObjectiveCRenderer.ts +++ b/packages/quicktype-core/src/language/Objective-C/ObjectiveCRenderer.ts @@ -936,7 +936,10 @@ export class ObjectiveCRenderer extends ConvenienceRenderer { } if (property.type.kind === "date-time") { this.emitLine( - `if (dict[@"${objectiveCStringEscape(jsonName)}"] && [dict[@"${objectiveCStringEscape(jsonName)}"] rangeOfString:@"^[0-9]{4}-[0-9]{2}-[0-9]{2}T" options:NSRegularExpressionSearch].location == NSNotFound) return nil;`, + `if (dict[@"${objectiveCStringEscape(jsonName)}"] && [dict[@"${objectiveCStringEscape(jsonName)}"] rangeOfString:@"^[0-9]{4}-[0-9]{2}-[0-9]{2}[Tt](?:[01][0-9]|2[0-3]):[0-5][0-9]:[0-5][0-9](?:\\\\.[0-9]+)?(?:[Zz]|[+-](?:[01][0-9]|2[0-3]):[0-5][0-9])?$" options:NSRegularExpressionSearch].location == NSNotFound) return nil;`, + ); + this.emitLine( + `if (dict[@"${objectiveCStringEscape(jsonName)}"] && ![[[[NSISO8601DateFormatter new] stringFromDate:[[NSISO8601DateFormatter new] dateFromString:[[dict[@"${objectiveCStringEscape(jsonName)}"] substringToIndex:10] stringByAppendingString:@"T00:00:00Z"]]] substringToIndex:10] isEqualToString:[dict[@"${objectiveCStringEscape(jsonName)}"] substringToIndex:10]]) return nil;`, ); } if ( diff --git a/packages/quicktype-core/src/language/Php/PhpRenderer.ts b/packages/quicktype-core/src/language/Php/PhpRenderer.ts index f468f82635..4d3dde8097 100644 --- a/packages/quicktype-core/src/language/Php/PhpRenderer.ts +++ b/packages/quicktype-core/src/language/Php/PhpRenderer.ts @@ -798,6 +798,14 @@ export class PhpRenderer extends ConvenienceRenderer { (transformedStringType) => { if (transformedStringType.kind === "date-time") { this.emitLine("$tmp = ", "new DateTime(", args, ");"); + this.emitLine("$errors = DateTime::getLastErrors();"); + this.emitBlock( + "if ($errors && ($errors['warning_count'] || $errors['error_count']))", + () => + this.emitLine( + "throw new Exception('Invalid date-time');", + ), + ); this.transformDateTime(className, "", ["$tmp"]); this.emitLine("return $tmp;"); return; From 8dd801363fbde4b87c9ae9abe3e030910c3e2ce3 Mon Sep 17 00:00:00 2001 From: Mark Probst Date: Mon, 7 Sep 2026 17:48:29 -0700 Subject: [PATCH 3/3] Add native date-time validation coverage --- test/inputs/schema/date-time.12.fail.date-time.json | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 test/inputs/schema/date-time.12.fail.date-time.json diff --git a/test/inputs/schema/date-time.12.fail.date-time.json b/test/inputs/schema/date-time.12.fail.date-time.json new file mode 100644 index 0000000000..fe33668ae1 --- /dev/null +++ b/test/inputs/schema/date-time.12.fail.date-time.json @@ -0,0 +1,7 @@ +{ + "date": "1985-04-12", + "time": "23:20:50.52Z", + "date-time": "2023-00-15T12:00:00Z", + "union-array": ["1985-04-12", "23:20:50.52Z"], + "complex-union-array": ["2018-08-13T21:31:01+00:10", "foo", 123] +}