diff --git a/lib/openapi_kit/codec/date_time.rb b/lib/openapi_kit/codec/date_time.rb index d064cb7..78e41a4 100644 --- a/lib/openapi_kit/codec/date_time.rb +++ b/lib/openapi_kit/codec/date_time.rb @@ -26,8 +26,13 @@ def self.from_wire(value) end end + # Rails renders a Time through as_json at three decimal places + # (ActiveSupport::JSON::Encoding.time_precision), so an application moving onto + # openapi_kit keeps emitting the timestamps its clients already parse. + PRECISION = 3 + sig { override.params(value: ::Time).returns(OpenAPIKit::Wire) } - def self.to_wire(value) = value.utc.iso8601 + def self.to_wire(value) = value.utc.iso8601(PRECISION) end end end diff --git a/spec/openapi_kit/codec_spec.rb b/spec/openapi_kit/codec_spec.rb index 4512785..80965e8 100644 --- a/spec/openapi_kit/codec_spec.rb +++ b/spec/openapi_kit/codec_spec.rb @@ -34,7 +34,13 @@ describe "round trips" do it "normalises a date-time to UTC" do value = described_class::DateTime.from_wire("2026-09-02T12:00:00+02:00") - expect(described_class::DateTime.to_wire(value)).to eq("2026-09-02T10:00:00Z") + expect(described_class::DateTime.to_wire(value)).to eq("2026-09-02T10:00:00.000Z") + end + + it "keeps the sub-second precision Rails renders, so a migrating client sees no change" do + value = described_class::DateTime.from_wire("2026-09-02T12:00:00.250Z") + + expect(described_class::DateTime.to_wire(value)).to eq("2026-09-02T12:00:00.250Z") end it "keeps decimal precision as a string" do diff --git a/spec/scripts/round_trip.rb b/spec/scripts/round_trip.rb index b94cb34..2444130 100644 --- a/spec/scripts/round_trip.rb +++ b/spec/scripts/round_trip.rb @@ -48,7 +48,7 @@ def check(label) check("differing values are unequal") { KitchenSink::Types::Mod::Codec.from_wire(WIRE.merge("id" => 8)) != mod } wire = KitchenSink::Types::Mod::Codec.to_wire(mod) -check("dumped date-time") { wire["updatedAt"] == "2026-09-02T10:00:00Z" } +check("dumped date-time") { wire["updatedAt"] == "2026-09-02T10:00:00.000Z" } check("dumped enum") { wire["status"] == "under-moderation" } check("required nullable stays null") { wire.key?("deletedAt") && wire["deletedAt"].nil? } check("absent optional omitted") { !wire.key?("summary") }