skip to content

Why does a shared serializer pin explicit date-time and enum representations instead of relying on defaults?

level: middleimportance: should knowfreq 50%

answer

  1. a default is not a contract
  2. offset and precision
  3. the constant name is an identifier
  4. unrecognised value on read

basics

~20 s

Defaults vary by serializer and version, and both representations leak code details onto the wire: an enum's declared constant name changes when someone renames it, and a numeric or zone-less timestamp drops information the reader needs.

solid answer

~50 s

Pin the instant format to one unambiguous form with an offset, decide the fractional-second precision, and state whether anything else is accepted on read. For enumerated values, map each constant to an explicit code rather than writing its declared name, because the name is a code identifier and renaming it is supposed to be a refactor — not a published change. Encoding the ordinal position is worse still, since inserting a member re-points every existing value. Then decide what the reader does with a value it does not recognise: reject when you own every producer, or map to a designated unknown member when newer producers may legitimately send members this deployment has not heard of. All of it belongs on the shared instance, alongside converters for domain value types, so the same type serializes identically everywhere it appears.

go deeper

for a junior

Know that timestamps should be written in one agreed textual form that carries an offset, and that enumerated values on the wire should be explicit codes rather than whatever the constant happens to be called.

for a middle

Explain why defaults are unsafe: they differ across serializers and versions, and both a constant name and an ordinal position couple the published payload to code that people refactor freely.

for a senior

Demonstrate the read-side judgment — precision round-trips, offsets, and whether an unrecognised enumerated value should be rejected or mapped to a designated unknown member for the producers you actually have.

for a principal

Set these as estate-wide conventions with a guard that fails the build on change, and weigh strict rejection against tolerant reading given how independently the services around you deploy.

## Why representation is a decision rather than a default A serializer must choose *some* textual or numeric form for a timestamp and for an enumerated constant. If the configuration does not choose, the library's default chooses — and that default varies between serializers, between major versions of the same serializer, and sometimes with which optional modules are present. That makes the public shape of a payload a function of a dependency graph, which is precisely what an API contract must not be. Both kinds of value also share a second problem: their natural default is **code-shaped**. The declared name of an enumerated constant is an identifier chosen by a programmer; a timestamp's default form often mirrors the internal type that happens to hold it. Publishing either is publishing an internal detail. ## Date and time: what has to be pinned - **The form.** A textual date-time carrying an offset, or an epoch number. Pick one and state it. - **The precision.** Fractional seconds are where round-trips break: a value written with nanosecond precision and read back into a millisecond-precision type is no longer equal to itself, which shows up in equality checks, deduplication and anything that signs a payload. - **The meaning.** An instant, a wall-clock date-time, and a calendar date are three different things. A deadline is usually an instant; a birthday is a calendar date and acquires a spurious time of day the moment it passes through a date-time type. - **Read leniency.** Decide whether a value arriving without an offset is rejected or interpreted, and if interpreted, against which zone. Silence here means the reader's host configuration decides. | Wire form | What it carries | The trap | |---|---|---| | Text with offset | an instant plus the offset it was produced in | precision and format vary unless pinned | | Text without offset | wall-clock fields only | every reader supplies its own zone, and they differ | | Epoch number | an instant | the unit is not in the value; precision is lost below it | | Date only | a calendar day | becomes an instant if it round-trips through a date-time type | ## Enums: the constant's name is an identifier, not a contract The common default is to write the declared name of the constant. That is convenient and it couples the wire to the code: renaming a constant is an ordinary refactor that silently changes a published value. Encoding the constant's **ordinal position** instead is worse — reordering the declaration, or inserting a member in the middle, re-points every previously written value. The durable approach is an explicit code per constant, declared once and decoupled from the identifier, so that the code is the contract and the identifier is free to change. Two further decisions come with it: 1. **Case and whitespace on read** — is an incoming value matched exactly, or normalised first? 2. **Unrecognised values on read** — reject, or map to a designated unknown member? Rejecting is right when you own the producer and want mismatches loud. A designated unknown member is right when third parties or newer producers may legitimately send members this deployment has never heard of; it lets an older reader keep working instead of failing the whole document. ## Custom converters are where these decisions live A converter is a pair of functions, one writing a type to a document node and one reading it back. Registering one on the shared instance is what makes a decision global: - the same type serializes identically wherever it appears — top level, nested, inside a collection, as a map value, inside a framework-generated error body; - domain value types (money, quantities, opaque identifiers) stop leaking their internal structure; - both directions stay symmetric. A converter registered only for writing produces an API that emits one shape and refuses to accept it back, which is a defect that integration tests on a single direction miss. ## Pin it with tests, not with a comment Because the risk here is silent change rather than failure, the guard is a stored sample document compared byte for byte after serializing a representative model containing each affected type. A dependency upgrade that moves a default then fails the build, which is exactly the event the pinning existed to catch.

  • An unrecognised enumerated value arrives in a request. Reject it or tolerate it?
    It depends on who produces it. If you own every producer, reject: an unknown value means deployment skew or a bug and should be loud. If third parties or newer producers send values first, resolve to a designated unknown member so an older reader keeps working, and make sure that member cannot silently take part in business decisions.
  • Why is fractional-second precision worth pinning explicitly?
    Because a value written at higher precision and read into a lower-precision type comes back unequal to what was sent. That breaks equality checks, deduplication by timestamp, idempotent replays and anything that signs or hashes the payload — all of which fail intermittently rather than outright, which makes them expensive to diagnose.

saying these in an interview costs you the question

  • Serializes enumerated values by declared constant name and calls renames safe refactors.
  • Emits zone-less timestamps and expects every reader to assume the same zone.
  • Uses an enumerated constant's ordinal position on the wire to save bytes.
  • Sends epoch numbers without stating anywhere whether the unit is seconds or milliseconds.
  • Registers a converter for writing only, leaving the read direction on the default.