In this article
  1. Before you start
  2. Why interviewers ask this
  3. Serialization and parsers from first principles
  4. Worked example
  5. Trade-offs and edge cases
  6. How to answer it in an interview
  7. Common mistakes
  8. Follow-up questions
  9. Sources

Before you start

This page belongs to the machine coding rounds guide, where serialization shows up as a coding task.

Why interviewers ask this

A serializer that works on friendly input and corrupts data on awkward input is a production bug, and that gap is what these questions expose. Candidates report three recurring tasks: code a key-value serializer where keys and values can contain any character, write a JavaScript Object Notation (JSON) parser and review it with the team, and serialize or verify a binary tree.

A weak answer joins strings with a comma and splits them back, and never tests a value that contains a comma. A strong answer names the rule that makes the round trip lossless, tests the empty string, a separator inside a value and a non-ASCII (American Standard Code for Information Interchange) character, and states the cost as linear in the input size. A Staff-level answer adds what happens on malformed or hostile input: reject it with a position, never guess, and never run code from it.

Serialization and parsers from first principles

Serialization
Serialization is the process of converting an in-memory data structure, such as a map or a tree, into a flat sequence of characters or bytes that can be stored or sent, so that the reverse step, called deserialization, can rebuild an equal structure from it later.
Parser
A parser is a program that reads a sequence of characters or bytes from left to right, checks each part against a format's grammar, and either builds a data structure from it or reports an error at the position where the input stops following the rules.
Length prefix
A length prefix is a number written before a string that says how many bytes follow, so a reader takes exactly that many bytes without searching for a closing marker, and the string can hold any character at all.

In plain words

Think of posting a parcel with a packing slip. The slip says "three items: a book, a mug, a scarf". The receiver does not guess where one item ends; the slip tells them. The analogy breaks in one place: a person can improvise, while a parser follows its rules exactly and fails on the first line that does not match.

In one precise sentence, serialization maps a structure to a string so that a deterministic parser can map it back to an equal structure. A grammar is the set of rules that says which strings are valid in a format and how they break into parts.

Step 1: The grammar fixes what a value can be

A format starts with a grammar. The JavaScript Object Notation (JSON) grammar in Request for Comments (RFC) 8259, a standard from the Internet Engineering Task Force (IETF), describes objects, arrays, strings, numbers and the three words true, false and null. Your own formats need the same discipline: list the shapes a value can take before writing code.

For a key-value store, the shapes are: a sequence of pairs, each pair a key string and a value string. For a binary tree, the shapes are: empty, or a value followed by a left tree and a right tree. Input: a description of your data. Output: a list of shapes. You now know what the serializer must write and the parser must accept. Nothing in the grammar says how a string ends; the next step decides that.

Step 2: A length prefix marks where each string ends

The serializer writes each string as its byte length, a separator, then the bytes. A reader that sees the number takes exactly that many bytes, whatever they contain, so a comma, a colon, a quote or a newline inside a value cannot confuse it. This is the answer to "key and value can contain any character".

The other common fix is escaping, which is rewriting each special character as a longer sequence so it cannot be mistaken for a separator. JSON uses it: RFC 8259 says the quotation mark, the backslash and the control characters U+0000 through U+001F must be escaped inside a string. Escaping works, but the reader must track whether the previous character was an escape.

Count bytes, not characters: write the length of the encoded form (for example Unicode Transformation Format 8-bit, UTF-8, which RFC 3629 defines as one to four bytes per character), or a reader decoding bytes will drift. Input: strings. Output: one flat byte sequence.

Step 3: The parser reads with a cursor

The parser keeps one position, the cursor, and only moves it forward. For the length-prefixed format the loop is: read digits up to the separator, convert them to a number n, take the next n bytes as a string, move the cursor past them, repeat until the end. Every step consumes input, so the total work is linear in the input size.

For a recursive format such as a tree, the parser calls itself: read one token (a token is one comma-separated piece, a number or #); if it marks an empty tree, return nothing; otherwise read the left subtree, then the right. Input: a flat sequence. Output: the rebuilt structure. A JSON parser is the same idea with more shapes: the next character decides which shape follows, and the parser calls the matching routine for objects, arrays, strings or numbers.

Step 4: The parser rejects what the grammar forbids

A parser that guesses past an error hides the bug until later. Instead it stops where the input breaks the grammar, and reports the position: a length that runs past the end, a missing separator, a tree that ends before every slot is filled.

Even a valid grammar leaves edge cases to decide. RFC 8259 says object names should be unique, that behaviour on repeated names is unpredictable, and that many implementations keep only the last pair. It also allows implementations to limit the range and precision of numbers, and gives 1E400 as an example that may cause interoperability problems. Decide your own policy, write it down and test it. Input: a possibly bad sequence. Output: a structure or a precise error.

A round trip needs one agreed end for every valueFlow of four stages: a grammar fixes the shapes, the serializer writes a length before each string, the parser reads with a cursor, and bad input is rejected with a position.Grammarallowed shapesSerializerlength, thenbytesParsercursor movesforwardRejecterror andpositionDiagram: Interview Clozo, CC BY 4.0 ·interview.clozo.ai/blog/serialization-and-parsers.html

A round trip works when the grammar, the writer and the reader agree on where every value ends.CC BY 4.0 — source: How do serialization and parsers work?
Step-by-step description
  1. List the shapes a value can take.
  2. Write a byte length before each string.
  3. Read forward, taking exactly that many bytes.
  4. Stop with a position on bad input.
  • A grammar fixes what shapes a value can take.
  • A length prefix says how many bytes follow.
  • A parser reads with a cursor that only moves forward.
  • A parser rejects input that breaks the grammar and reports the position.

Knowing it is half the job. Saying it is the other half.

Interview Clozo is an AI interview copilot for real interview rounds: it hears the question and puts a strong answer on your screen, and you say it in your own words. See the features or how it works.

Start Free Trial →

Worked example

The input is four key-value pairs (illustrative): name is Ada, note is a,b:c, empty is the empty string, and city is Zürich €. The raw text of all keys and values is 36 bytes in UTF-8.1

The naive format joins each pair as key:value with commas between pairs. It fails the round trip: the value a,b:c contains both separators, so splitting on commas cuts it in two.1 The length-prefixed encoding is 4:name3:Ada4:note5:a,b:c5:empty0:4:city11:Zürich €. It decodes back to the same four pairs, and takes 53 bytes.1 The value Zürich € has eleven bytes because the letter ü takes two bytes and the euro sign takes three.1

Length prefixes cost 17 bytes more than the raw 36-byte payload, while JSON costs 25 bytes more, or 32 bytes more when non-ASCII characters are escaped.
Encoding of the four pairsSize in bytes (computed)Round trip
Raw keys and values, no separators36not decodable
Naive comma and colon join43fails on a,b:c
Length prefix53succeeds
JSON, UTF-861succeeds
JSON, non-ASCII escaped68succeeds
Length prefixes add less overhead than JSON on four pairsBar chart in bytes for the same four key-value pairs: 36 raw, 53 with length prefixes, 61 as JSON in UTF-8 and 68 as JSON with non-ASCII characters escaped.Size (bytes)020406080RawLength prefixJSONJSON escapedEncoding36536168Diagram: Interview Clozo, CC BY 4.0 ·interview.clozo.ai/blog/serialization-and-parsers.html
Length prefixes and JSON both add overhead to the 36-byte payload; the prefixes cost less here.CC BY 4.0 — source: How do serialization and parsers work?
Text description of the diagram
  • The raw keys and values take 36 bytes.
  • The length-prefixed encoding takes 53 bytes.
  • JSON takes 61 bytes in UTF-8 and 68 bytes with non-ASCII characters escaped.

Now a tree. The binary tree with root 1, left child 2 (children 4 and 5) and right child 3 has five nodes. Preorder with a marker # for each empty child gives 1,2,4,#,#,5,#,#,3,#,#, which is eleven tokens, one more than twice the node count, and rebuilds the identical tree.1

To verify such a string without building the tree, keep a count of open slots, starting at one. Each token fills a slot; a node token opens two new ones. The count after each token is 2, 3, 4, 3, 2, 3, 2, 1, 2, 1, 0, and the string is valid because it reaches zero exactly at the last token.1 The input 1,# ends with one slot still open, and #,1,#,# fails at its second token because no slot is left.1 One pass with one counter is linear time, and constant extra space if you scan the string for commas instead of calling split, which builds a list of every token.

def lp_encode(pairs):
    parts = []
    for k, v in pairs:
        for s in (k, v):
            b = s.encode("utf-8")           # count bytes, not characters
            parts.append(str(len(b)).encode() + b":" + b)
    return b"".join(parts)                   # join once, not += in a loop

def lp_decode(data):
    items, i = [], 0
    while i < len(data):
        j = data.find(b":", i)
        if j < 0:
            raise ValueError(f"missing separator after byte {i}")
        if not data[i:j].isdigit():          # rejects empty, signed or non-numeric lengths
            raise ValueError(f"bad length at byte {i}")
        n = int(data[i:j])
        if j + 1 + n > len(data):
            raise ValueError(f"length {n} at byte {i} runs past the end")
        items.append(data[j + 1:j + 1 + n].decode("utf-8"))
        i = j + 1 + n                        # the cursor only moves forward
    if len(items) % 2:
        raise ValueError("odd number of strings")
    return [(items[x], items[x + 1]) for x in range(0, len(items), 2)]

The decoder moves its cursor past each byte once, so it is linear in the input size. In Python, bytes are immutable, so repeated += copies the buffer each time; collecting parts in a list and joining once keeps the encoder linear. The three checks reject a missing separator, a length that is not plain digits (so -1 fails) or runs past the end, and an odd number of strings, each with a position or a clear message.

Trade-offs and edge cases

Each way of marking a string's end trades simplicity, readability and safety. A bare delimiter is simplest and wrong for arbitrary data. Escaping keeps the text readable, as in JSON, but needs state in the reader. A length prefix handles any bytes, but a person cannot read it, and one wrong count corrupts everything after it.

Only escaping and length prefixes let values hold any characterTable comparing a bare delimiter, escaping and a length prefix by whether any character is allowed, what the reader must do, and where each fails.QuestionBaredelimiterEscapingLengthprefixAny characterallowedNoYesYesReader mustSplit on amarkTrackescapesCount bytesFails whenData has themarkAn escape ismissingThe count iswrongHuman readableYesMostlyPoorlyDiagram: Interview Clozo, CC BY 4.0 ·interview.clozo.ai/blog/serialization-and-parsers.html
Only escaping and length prefixes let a value contain any character; a bare delimiter does not.CC BY 4.0 — source: How do serialization and parsers work?
Text description of the diagram
  • A bare delimiter breaks when a value contains it.
  • Escaping needs the reader to track escape characters.
  • A length prefix needs a correct byte count.

Edge cases to test: the empty string (length zero, as in 0: above), a value that looks like a length, a multi-byte character, a key repeated twice, and input cut off mid-value. A graph with cycles or shared nodes needs more than a tree format: give each node an id and write references to ids, or the writer never stops. The same idea of parsing structured text appears in event and log processing problems, and a store you must save and restore appears in the in-memory database assessment and the least recently used (LRU) cache design. Other low-level design questions ask for the same split of encoding and policy.

Format choice also has a safety side. The Python documentation warns that the pickle module is not secure: malicious pickle data can execute arbitrary code during unpickling, so only data you trust may be unpickled. A text format such as JSON builds only plain data, which is why it is the usual choice for input from outside. Recursive parsers also need a plan for deeply nested input, such as a depth limit.

How to answer it in an interview

Interviewer asks

Write serialize and deserialize for a key-value store where keys and values are strings that can contain any character.

A strong answer

I would not use a delimiter, because any delimiter can appear inside a value. I write each string as its UTF-8 byte length, a colon, then the bytes. The decoder reads digits up to the colon, takes exactly that many bytes, and moves its cursor forward, so both directions are linear. I would test the empty string, a comma and a colon inside a value, and a non-ASCII character. On malformed input, such as a length past the end, I raise an error with the position rather than guess.

The spoken structure: reject delimiters, state the length-prefix format, give the cost, then name your tests and the error policy.

Common mistakes

  • "I will join pairs with a comma and a colon." (wrong) A value can contain both. (right) "I use length prefixes, or escape the separators and the escape character itself."
  • "The length is the number of characters." (wrong) The reader takes bytes, so a multi-byte character breaks the count. (right) "I write the length of the encoded bytes."
  • "Parsing is just splitting the string." (wrong) Nested formats need a cursor and recursion. (right) "I parse with a cursor and call a routine per shape."
  • "On bad input I return what I have." (wrong) Half a structure looks valid and spreads the bug. (right) "I stop with an error and the position."
  • "Deserializing is always safe." (wrong) Some formats run code while loading. (right) "I use a data-only format for untrusted input."

Follow-up questions

How do you serialize a key-value store when keys and values can contain any character?

Use length prefixes: write each string's byte length, a separator, then the bytes. The reader takes exactly that many bytes, so no character in a key or value can be mistaken for a separator.

How do you write a JSON parser and review it with a team?

Write one routine per grammar shape (object, array, string, number, literal) driven by a cursor, following RFC 8259. In review, a strong answer checks the escape rules, \u surrogate pairs (RFC 8259 warns about unpaired surrogates such as \uDEAD), repeated object names, number limits, error positions and a depth limit, and asks for tests on each.

How do you check that a preorder serialization of a binary tree is valid, and what does it cost?

Keep a count of open slots, start at one, subtract one for every token and add two for every non-empty node. Reject if the count hits below zero early, and accept only if it ends at zero; it is linear time, and constant extra space if you scan for commas rather than calling split, which stores every token.

Is it safe to deserialize data from anyone?

Not with every format. The Python documentation says unpickling untrusted data can execute arbitrary code, so use a data-only format and validate the result.

How do you serialize a graph that has cycles?

Give every node an id, write each node once with the ids of its neighbours, and rebuild in two passes: create all nodes, then connect them. Writing nested copies would loop forever on a cycle. The nested list problem shows the recursive case without cycles.

How do you serialize and deserialize a binary tree?

Write the tree in preorder and put # for every empty child; read it back by consuming tokens recursively.

def serialize(node):                         # node is (value, left, right) or None
    parts = []
    def walk(n):
        if n is None:
            parts.append("#")
            return
        parts.append(str(n[0]))
        walk(n[1])
        walk(n[2])
    walk(node)
    return ",".join(parts)

def deserialize(text):
    def build(it):
        tok = next(it)
        if tok == "#":
            return None
        return (int(tok), build(it), build(it))   # left is read before right
    return build(iter(text.split(",")))

Time is O(n) and the output has 2n+1 tokens for n nodes. The recursion stack is O(h), where h is the tree height, so a very deep tree can hit the language's recursion limit; use an explicit stack instead. With the markers the string is unambiguous: the slot count from the previous question shows where every subtree ends, so each token has one possible place. Preorder without markers is not: 1,2,3 fits a chain and a tree with two children of the root.

Key takeaways
  • Serialization writes a structure as text or bytes; a parser reads it back against a grammar.
  • A delimiter alone fails when data contains it; use a length prefix or escaping.
  • Length prefixes count bytes, not characters.
  • A parser moves a cursor forward and rejects bad input with a position.
  • Never load untrusted data with a format that can run code.

Sources

  1. The JavaScript Object Notation (JSON) Data Interchange Format (RFC 8259) — IETF / RFC Editor (T. Bray), 13 Dec 2017
  2. UTF-8, a transformation format of ISO 10646 (RFC 3629) — IETF / RFC Editor (F. Yergeau), 1 Nov 2003
  3. pickle - Python object serialization — Python Software Foundation (read 2026-10-26; page undated), 26 Oct 2026