A fleet's uplink switches from JSON to MessagePack with no other change: which decoded values can quietly differ, and why?
answer
- same model, wider type set
- one numeric syntax becomes two types
- Base64 field becomes a byte string
- keys need not be strings any more
- infinity and not-a-number now expressible
basics
~20 sMostly it is a clean swap, but the binary model is richer, so numbers can arrive as an integer or a float rather than one numeric text form, raw bytes stop being Base64 text, non-string map keys and non-finite floats become expressible, and consumers switching on type see cases the text form could not produce.
solid answer
~50 sThe document model survives, which is why the migration looks drop-in — but the binary model is *wider* than the text one in four places, and each is a place a consumer can break. First, **numbers**: a text encoding has one numeric syntax, while the binary encoder chooses between an integer and a floating-point value and picks a width, so a value that always arrived as `1.0` may now arrive as the integer `1`. Second, **byte strings**: a field that was Base64 text becomes raw bytes with a different type tag, and code that expected a string will not match. Third, **map keys**: some encodings in this family allow any value as a key, so a generic mapper can produce a map the text form could not express. Fourth, **non-finite floats**: infinities and not-a-number are representable in binary and have no text form, so they pass silently until something renders back to text. Record framing is a separate concern and does not come free with the swap.
go deeper
Know that the field names and structure survive the swap, and that a binary record cannot be read with the eye or a text editor the way the previous form could.
Explain the widened type set: separate integer and floating-point types, a byte-string type, and values such as infinity that had no text form at all.
Show the migration judgment: pin the numeric type per field at the producer, change Base64 consumers in the same release, and decode both forms into one shape while the fleet rolls over.
Treat a representation change that widens the type set as a contract change, and decide who owns the consumer inventory before a byte of firmware ships.
## Why the swap looks free Both sides speak the same document model, so in the common case you change one encoder and one decoder and every field arrives with the same name, shape and meaning. That is the genuine attraction on a fleet you cannot upgrade in step: the model is unchanged, so the *semantics* of a record do not move even while some devices are still emitting the old form. The trouble is that the binary model is not identical to the text model — it is a **superset** in four specific places, and a superset is exactly what breaks a consumer that was written against the narrower one. ## The four places values quietly differ **1. One numeric syntax becomes two numeric types.** A text encoding writes every number the same way and leaves it to the reader to decide whether it is an integer or a floating-point value. A binary encoder makes that decision itself and writes a type tag for it, choosing the narrowest width that fits. Consequences: - a value that a consumer always saw as a fractional number may now arrive tagged as an integer, and code that assumed a floating-point type fails or silently changes rounding behaviour; - a large integer that a text reader would have parsed into a double — losing exactness beyond the 53-bit significand of IEEE 754 — now arrives as an exact 64-bit integer, which is *better fidelity* but a change in what downstream code receives; - round-tripping binary → text → binary can change the type back again, because the text form has nowhere to record which one it was. **2. Base64 text becomes a byte string.** A binary payload previously travelled as a text field of Base64 characters, costing four bytes for every three. In the binary encoding it travels as a **byte string**, a distinct type. A consumer that pattern-matches on "string" will not match it, and a consumer that decodes Base64 unconditionally will corrupt it. **3. Keys need not be strings.** Some members of this family allow any value — an integer, an array — as a map key, while one member requires string keys. A generic object mapper writing a structure with integer keys will produce a record no text encoding could have expressed, and a consumer that assumes string keys will trip on it. Decide and enforce string keys if your consumers assume them. **4. Non-finite floating-point values become expressible.** The binary float types can carry positive and negative infinity and not-a-number; the text form has no syntax for them, which is why such a value used to be rejected, stringified or nulled at the boundary. After the swap it travels intact — and then fails at whatever point downstream renders the record back to text. ## A migration checklist for a fleet you cannot upgrade in step 1. **Pin the numeric mapping.** Decide, per field, whether it is an integer or a floating-point value, and make the encoder emit that type rather than letting it infer one from the in-memory value. 2. **Enumerate the byte fields** that were Base64 and change their consumers at the same time — this is a contract change in disguise. 3. **Constrain keys to strings** unless a consumer genuinely benefits otherwise. 4. **Reject non-finite floats at the producer**, where you know what the reading meant, rather than at the renderer, where you do not. 5. **Run both forms through one decode path** during the rollout, so a device on old firmware and a device on new firmware land in the same object shape before any business logic sees them. | Field kind | Text form | Binary form | Risk if ignored | |---|---|---|---| | Whole number | numeric text | integer tag, narrowest width | consumer expecting a fractional type | | Fractional number | numeric text | floating-point tag | exactness and rounding change | | Raw bytes | Base64 text | byte string | type match fails, or double-decoding | | Map key | string only | any value, in some members | consumer assumes string keys | | Infinity / not-a-number | not expressible | expressible | fails later, at the text boundary | ## The thing that is genuinely unchanged No compatibility guarantee arrives with the new encoding. A reader that ignored unknown keys still ignores them; a reader that required a key still fails without it. The swap changes representation only, and the fields whose *representation* got wider are the fields to inspect. Say that plainly in an interview, then name the four cases — that combination is what separates someone who has run the migration from someone who has read about the format.
- Is the integer-versus-float distinction a regression or an improvement?It is better fidelity that breaks assumptions. A text reader parsing into a double loses exactness on integers beyond 53 bits of significand; the binary form keeps them exact. But a consumer that has always received a fractional type now sometimes receives an integer, so the fix is to pin the type per field at the producer rather than let the encoder infer it.
- How do you run the migration while both forms are in flight from the same fleet?Detect the form at the ingest edge — the first byte of a binary record is a type tag, not printable punctuation — and decode both into one internal object shape before any logic runs. Everything downstream then sees one representation, and the rollout finishes when the last device stops emitting the old form.
- Does the swap tell you where one record ends and the next begins?No. A text stream is often delimited by newlines because records contain none; a binary record can contain any byte, so the delimiter trick stops working and the stream needs explicit record boundaries. That framing decision is separate from the encoding choice and has to be made deliberately.
saying these in an interview costs you the question
- Calling it a pure representation swap with no consumer impact
- Assuming a number keeps the same type across the swap
- Leaving Base64 decoding in place over a byte-string field
- Assuming map keys are still guaranteed to be strings
- Expecting newline delimiting to keep working on a binary stream