Skip to content

feat(kotlin): Support for date and datetime from JSON schema#2845

Merged
schani merged 13 commits into
glideapps:masterfrom
nikhilunni:master
Jul 19, 2026
Merged

feat(kotlin): Support for date and datetime from JSON schema#2845
schani merged 13 commits into
glideapps:masterfrom
nikhilunni:master

Conversation

@nikhilunni

@nikhilunni nikhilunni commented Oct 31, 2025

Copy link
Copy Markdown
Contributor

Description

Add support for JSON Schema format: date-time, format: date, and format: time in the Kotlin code generator. These formats now generate proper java.time.* types instead of String.

This implementation follows the same pattern as Java's date-time support, using:

  • OffsetDateTime for format: "date-time"
  • LocalDate for format: "date"
  • OffsetTime for format: "time"

The feature works across all Kotlin frameworks: just-types, jackson, klaxon, and kotlinx.

Related Issue

Fixes #2460

Motivation and Context

We prefer Kotlin generation to Java generation for nullability propagation, but did not support datetime formatting, as Java does. Plus, saw there was a longstanding issue for this.

Previous Behaviour / Output

For the given JSON schema:

{
  "$schema": "http://json-schema.org/draft-07/schema#",
  "type": "object",
  "properties": {
    "eventDate": {
      "type": "string",
      "format": "date"
    },
    "eventTime": {
      "type": "string",
      "format": "time"
    },
    "plannedDateTime": {
      "type": "string",
      "format": "date-time"
    }
  }
}

It would output:

package quicktype

data class MySchema (
    val eventDate: String,
    val eventTime: String,
    val plannedDateTime: String
)

New Behaviour / Output

package quicktype

import java.time.OffsetDateTime
import java.time.LocalDate
import java.time.OffsetTime

data class MySchema (
    val eventDate: LocalDate,
    val eventTime: OffsetTime,
    val plannedDateTime: OffsetDateTime
)

How Has This Been Tested?

Manual Testing

  • Tested with the existing test/inputs/schema/date-time.schema file
  • Verified output for all Kotlin frameworks:
    • --framework just-types
    • --framework jackson
    • --framework klaxon
    • --framework kotlinx
  • Tested with the exact schema from issue Kotlin date and datetime types are generated as strings from json schema #2460
  • Verified proper handling of:
    • Simple date-time properties
    • Date-time types in unions
    • Date-time types in arrays
    • Complex nested structures with date-time types

Automated Testing

  • Added "date-time" feature to KotlinLanguage test configuration
  • Added "date-time" feature to KotlinJacksonLanguage test
    configuration
  • Follows the same pattern as the Golang date-time feature
    implementation (commit 1f89a97)

@nikhilunni

Copy link
Copy Markdown
Contributor Author

Could somebody please take a look at this?

schani and others added 5 commits July 9, 2026 15:47
Only conflict was the import block in Kotlin/language.ts: kept master's
ESM .js import paths and re-added the PR's type imports
(PrimitiveStringTypeKind, TransformedStringTypeKind, StringTypeMapping)
in the same style.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Register strict ISO converters on the generated ObjectMapper for
OffsetDateTime, LocalDate and OffsetTime via the existing convert()
infrastructure instead of requiring jackson-datatype-jsr310: the
module's serializers don't round-trip faithfully (OffsetTime pads
"23:20:50.52Z" to "23:20:50.520Z"), while the ISO formatters do,
and this way generated code needs no extra dependency.

Also fix union deserialization when several members are guarded by the
same node type: date, time, date-time and enum values are all TextNode,
so the previous output had duplicate 'is TextNode ->' branches of which
only the first could ever match. Members sharing a guard now parse in
a single branch that tries each member in sequence, strictest first
(transformed strings, then enums, then plain strings).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The klaxon renderer's matchType calls were missing the
transformedStringType matcher the base renderer maps to java.time
types, so any schema with date/time formats in a union crashed
generation with "Unsupported type date-time in non-exhaustive match".
Add the matchers, register Klaxon converters that parse the java.time
types from ISO strings and re-serialize them via the ISO formatters
(exact round-trip), and give union members that share the 'is String'
guard the same sequential-parse treatment as the Jackson renderer.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…va.time

With date/time inference now mapping to java.time types, two groups of
misc inputs fail the round-trip comparison: files with non-RFC3339
date-times (space-separated, no offset) that OffsetDateTime.parse
rejects, and files whose fractional seconds have trailing zeros that
the ISO formatters legitimately trim (".000Z" -> "Z"). Skip them for
both Kotlin variants, mirroring what Go already does for the first
group.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@schani

schani commented Jul 12, 2026

Copy link
Copy Markdown
Member

Update on the CI failures: the fresh run on 379134e confirmed two defects in the feature, and while fixing them we found a third:

  1. Klaxon generation crash (masked in CI by fail-fast): matchType in KotlinKlaxonRenderer.ts is missing the transformedStringType matcher, so generating from test/inputs/schema/date-time.schema dies with "Unsupported type date-time in non-exhaustive match".
  2. Jackson runtime failure (the one CI shows): the generated mapper can't deserialize java.time.LocalDate — nothing handling java.time is registered.
  3. Duplicate union guards in both renderers: when branches like is TextNode -> ... repeated per date type, so only the first union member could ever parse.

We've prepared verified fixes for all three (round-trip byte-identical for both klaxon and jackson locally, fail-case correctly rejected, no regression on non-date output): nikhilunni#1 targets your master, so merging it on your side updates this PR and should turn kotlin CI green. Cherry-picking works too (branch: glideapps/quicktype:kotlin-datetime-fixes-2845) — or say the word and we'll push directly to this PR.

schani and others added 4 commits July 12, 2026 20:00
…erence

With the default recognizer now requiring RFC 3339 (T separator and
timezone offset), space-separated timestamps like "2013-06-15 21:10:28"
are inferred as plain strings, so these eight misc fixtures round-trip
for both kotlin variants without skips (verified per file; f6a65.json
also verified end-to-end with kotlinc).

The nine remaining skips are unchanged: their values are RFC 3339-valid
but carry trailing-zero fractional seconds (e.g. ".000Z") that
java.time does not re-serialize identically.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The Kotlin language's stringTypeMapping maps JSON Schema date, time, and
date-time formats to java.time types for every framework, but the
kotlinx renderer was never taught about them: it emitted @serializable
data classes with OffsetDateTime/LocalDate/OffsetTime properties, which
don't compile because kotlinx.serialization has no built-in serializers
for java.time ("Serializer has not been found for type
'OffsetDateTime'").

Emit custom KSerializer objects for the java.time types that appear in
the type graph and register them file-wide with @file:UseSerializers.
Like the Jackson and Klaxon converters, they parse with .parse and
format with the ISO formatters so values round-trip faithfully.

There is no kotlinx fixture in CI, so cover this with a unit test
asserting the serializers and file annotation are emitted (and not
emitted when no date/time types occur).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
# Conflicts:
#	packages/quicktype-core/src/language/Kotlin/KotlinJacksonRenderer.ts
#	packages/quicktype-core/src/language/Kotlin/KotlinKlaxonRenderer.ts
#	packages/quicktype-core/src/language/Kotlin/KotlinXRenderer.ts
schani and others added 2 commits July 19, 2026 13:29
…erence

Merging master brought in the kotlinx fixture coverage from glideapps#2950,
whose skip lists were tuned against master, where Kotlin had no
date-time support. With this branch's date/time mapping:

- Declare the "date-time" feature for KotlinXLanguage. date-time.schema
  itself stays skipped because its union-array properties hit the
  kotlinx sealed-class union limitation; the emitted KSerializers are
  exercised by JSON inputs with inferred date-times instead.
- Skip the misc JSON inputs whose date-times carry trailing zeros in
  the fractional seconds, which java.time doesn't re-serialize
  identically — the same set already skipped for klaxon and jackson.
- Skip date-time-or-string.schema: its string|date-time property was a
  plain string before and now becomes a union.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Comment thread test/languages.ts Outdated
Instead of skipping test inputs whose date-time strings java.time does
not re-serialize byte-identically, give Kotlin a DateTimeRecognizer
(the existing facility for this, mirroring SwiftDateTimeRecognizer)
that refuses to infer date/time types from such strings in the first
place. Rejected strings simply stay plain strings.

The rules, all verified against java.time's actual parse/format
behavior (ISO_LOCAL_DATE / ISO_OFFSET_TIME / ISO_OFFSET_DATE_TIME):

- no trailing zeros in fractional seconds (java.time formats the
  shortest fraction: ".000" disappears, ".500" becomes ".5"),
- at most 9 fractional digits (java.time's nanosecond precision;
  more don't parse),
- offset "Z" or a nonzero "±hh:mm" within java.time's ±18:00 range
  (zero offsets format back as "Z", larger ones don't parse),
- uppercase "T" and "Z" (lowercase forms format back in uppercase),
- no Feb 29 outside leap years (java.time refuses to parse it).

This only affects inference from JSON samples — JSON Schema's
"format": "date-time" maps to OffsetDateTime regardless, which is the
correct behavior for schema-driven input.

The nine date-time-related misc-JSON skips on the Kotlin fixtures are
lifted; every date/time-like string in the test inputs that the new
recognizer accepts was verified to round-trip byte-identically through
java.time (5692 strings checked). 77392.json and b4865.json stay
skipped for kotlinx only, for the unrelated top-level-JsonArray reason.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@schani
schani merged commit 3665111 into glideapps:master Jul 19, 2026
25 checks passed
@schani

schani commented Jul 19, 2026

Copy link
Copy Markdown
Member

Thank you so much, both for your contribution and your patience!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Kotlin date and datetime types are generated as strings from json schema

2 participants