Infer date-times only for strict RFC 3339 strings#2916
Merged
Conversation
The default DateTimeRecognizer, vendored from ajv, accepted two forms
that RFC 3339 does not: a space as the date/time separator, and times
without a timezone offset. As a result, JSON samples containing strings
like "2013-06-15 21:10:28" were inferred as date-times, and languages
with strict date parsers (Go, Swift, java.time, ...) generated code
that could not parse the very samples the types were inferred from.
Several languages carried test-skip lists to paper over this.
The separator must now be "T" (or "t"), and times must carry an offset
("Z"/"z" or "±hh:mm"), for both isDateTime and standalone isTime
(RFC 3339 full-time requires the offset). Non-conforming strings are
inferred as plain strings, which every target handles faithfully.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Pins the strict RFC 3339 behavior: rejects space separators and missing timezone offsets, accepts lowercase t/z and fractional seconds, and validates date/time field ranges. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Now that non-RFC 3339 strings ("2013-06-15 21:10:28") are no longer
inferred as date-times, the fixtures that contain them work for
languages with strict date parsers:
- golang: all 8 skipJSON entries round-trip (verified locally with go)
- swift: the 8 "date-time issues" skipDiffViaSchema entries pass -- the
divergence between SwiftDateTimeRecognizer (direct path) and the
lenient default (schema path) is gone for these files
- typescript/javascript/flow/typescript-zod: 7681c.json contains only
space-separated date-times, so no field is Date-typed anymore and the
"year 0 is out of range" runtime failure cannot occur (verified by
round-tripping the typescript, javascript, and zod fixtures locally;
flow left to CI)
Also removes entries that an audit showed were already stale
(passing even before this change): cplusplus and elm space-class
skipDiffViaSchema entries and csharp's 437e7.json (both variants).
Kept deliberately: python/rust skipDiffViaSchema entries (they still
diff, due to schema-path type naming, not date-times), all skips for
RFC 3339-valid files with fractional-second normalization issues
(0a358, 32d5c, 54d32, 5eae5, 77392, 80aff, 9ac3b, a0496, b4865, d23d5,
337ed), and the zod/effect-schema naming-related lists.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Behavior change (deliberate, affects all languages)
Strings that are not strict RFC 3339 date-times are no longer inferred as
date-time— they become plain strings. The default recognizer (vendored from ajv) accepted a space as the date/time separator and made the timezone offset optional, both RFC 3339 violations. JSON samples containing values like"2013-06-15 21:10:28"were typed as date-times, and every language with a strict date parser generated code that could not read the very samples the types were inferred from.The evidence this was wrong: five languages carried skip lists around it (Go skipped 8 misc fixtures outright; Swift, Python, Rust, C++ skipped overlapping diff-via-schema runs), and pending Kotlin work (#2845) would have needed 17 more skips. "date-time" in the JSON ecosystem (JSON Schema, OpenAPI) means RFC 3339.
What changed
packages/quicktype-core/src/DateTime.ts:DATE_TIME_SEPARATOR:/t|\s/i→/t/i— separator must beT/t, never a spaceTIME: offset group(z|[+-]\d\d:\d\d)?→ mandatory — applies to bothisDateTimeand standaloneisTimeDesign decision on
isTime: RFC 3339full-timerequires the offset, and JSON Schema'stimeformat isfull-time, so bare"21:10:28"no longer infers astimeeither. Lowercaset/zremain accepted (RFC 3339 allows them). Fractional seconds remain accepted.Verified type flips (generated locally):
"2013-03-21 03:59:02"-style fields are nowstringin Go/C#/Python output; RFC 3339 values ("1929-07-10T00:00:00Z","2010-01-12T00:00:00.000Z") stay typed.Skips removed (each verified individually)
skipJSON(8):f6a65 e0ac7 c3303 7681c 437e7 127a1 26b49 0cffa— all round-trip byte-identically through the real Go fixture locally (go run), and all pass diff-via-schema.skipDiffViaSchema(8): the same space-class files, marked// date-time issues. Root cause was recognizer divergence: the direct path usedSwiftDateTimeRecognizer(strict), the schema path used the lenient default, so the two paths inferred different types. Verified DIFF-on-master → PASS-with-this-change for these files.skipJSON(1 each):7681c.json("year 0 is out of range") — every date-like string in that file is space-separated, so no field isDate-typed anymore and the runtime parse can't fail. TypeScript and JavaScript fixtures round-trip locally; flow could not be run locally (no flow binary) — CI validates it. TypeScript's diff-via-schema for the file also passes.skipJSON(1):7681c.json— previouslyz.coerce.date()re-serialized the coerced values as ISO UTC, breaking comparison; now they stay strings. Verified by round-tripping the zod fixture locally.32d5c.jsonstays skipped (RFC 3339-valid values;.000Znormalization issue remains).skipDiffViaSchema, csharp both variants (437e7): the audit showed these were already stale — they pass even without this change (with a single shared recognizer, both diff-via-schema paths always inferred identically; only Swift's override caused real divergence). Removed as cleanup; attribution noted honestly here.Skips deliberately kept
skipDiffViaSchemaspace-class entries: still diff — the divergence is schema-path type naming (DownsizedvsThe480_WStill), not date-times.0a358 32d5c 54d32 5eae5 77392 80aff 9ac3b a0496 b4865 d23d5 337ed): RFC 3339-valid, still inferred as date-times; their issues are serialization normalization (e.g..000Z→Z), a separate problem.skipDiffViaSchemalists (naming-related; effect-schema still diffs) and the Java legacy-datetime variant skips (serialization strictness).Per-language recognizer verdict
SwiftDateTimeRecognizerstays. The strict default now agrees with it on separators/offsets, but Swift's recognizer also rejects fractional seconds (Swift'sISO8601DateFormattercan't parse them by default) and tolerates some sloppy field widths. Not redundant. No other language overrides the recognizer.Verification
t/z, accepts fractions, field-range validation).Follow-up (not in this PR)
Per-language "round-trip-provable" recognizers (e.g. rejecting
.000Zfor java.time-based targets) would eliminate the remaining normalization skips; and a leniency escape hatch could be added if users depended on MySQL-style stamps being typed (deliberately out of scope here).🤖 Generated with Claude Code