All articles
Backend from First Principles

Serialization & Deserialization: How Machines Talk Across Languages

Your JavaScript frontend speaks JS, your Rust backend speaks Rust. How do they communicate? Through the universal translator pattern of serialization.

2026-02-058 min read

Your JavaScript frontend speaks JavaScript. Your Rust backend speaks Rust. Your Go microservice speaks Go. They all need to talk to each other. How?

This is the third fundamental I'm revisiting in my backend-from-first-principles series. And it's one of those topics that sounds academic until you realize it's happening in every single HTTP request your application processes.


The Core Problem: Machines Don't Speak the Same Language

Picture this. You have a JavaScript frontend running in a browser. It collects a user's name and email into a nice little JS object:

const user = { name: "Ali", email: "ali@example.com", age: 25 };

This object needs to travel across the internet to a backend server written in Rust. But here's the thing — Rust has never heard of a JavaScript object. Rust has structs with strict, compiled types. It doesn't know what a loosely-typed, dynamic JS object is, and it can't parse one.

The reverse is equally true. A Rust struct means nothing to a Python server. A Go map means nothing to a Java backend.

Every language has its own internal way of representing data. And none of them are compatible with each other.

You can't ship a JavaScript object over the internet any more than you can mail a thought.


The Mental Model: The Universal Translator

Imagine the United Nations. Delegates from 50 countries sit in one room. Each speaks their own language. Direct communication is impossible — a Japanese delegate can't understand an Arabic delegate, who can't understand a Portuguese delegate.

The solution isn't forcing everyone to learn every language. The solution is a single common language that every translator in the room can convert to and from.

That's exactly what serialization and deserialization do.

Serialization = Converting your native data into the common language before sending it out

Deserialization = Converting the common language back into your native data after receiving it

JavaScript Object → serialize → JSON → network → JSON → deserialize → Rust Struct

The "common language" is the serialization format. And the most popular one on the web, by a massive margin, is JSON.


JSON: The Lingua Franca of the Internet

JSON (JavaScript Object Notation) dominates roughly 80% of HTTP API communication. Despite having "JavaScript" in its name, it belongs to no language — every major programming language has a JSON parser.

Why JSON won:

  • Human-readable — You can open a JSON payload and immediately understand it. Try that with a binary format.
  • Simple rules — Only 6 value types: string, number, boolean, array, object, and null. Nothing else. No functions, no classes, no executable code.
  • Universal support — Every language, every framework, every tool speaks JSON.

The rules are minimal:

{
  "name": "Ali",
  "age": 25,
  "isActive": true,
  "skills": ["backend", "devops"],
  "address": {
    "city": "Mumbai",
    "country": "India"
  },
  "middleName": null
}

Keys must be double-quoted strings. Values must be one of the six types. Curly braces wrap objects. Square brackets wrap arrays. That's the entire specification.


The Complete Flow: Following a Request End to End

Let's trace a real request from start to finish. A user fills out a registration form on a React frontend, and the data needs to reach a Rust backend.

Step 1 — Client prepares data. The frontend gathers input into a native JavaScript object:

const formData = { name: "Ali", email: "ali@example.com" };

Step 2 — Serialization (client side). The client calls JSON.stringify() to convert the JS object into a JSON string and attaches it to the HTTP request body:

POST /api/users
Content-Type: application/json

{"name":"Ali","email":"ali@example.com"}

At this point, the data is no longer a JavaScript object. It's a flat text string — a sequence of characters that any system can read.

Step 3 — Transmission. The network layer takes this text string and breaks it down further — into TCP segments, IP packets, and ultimately electrical signals (0s and 1s) traveling through cables or airwaves. You don't touch any of this. As a backend engineer, you operate at the Application Layer. The network handles the physics.

Step 4 — Deserialization (server side). The Rust server receives the JSON string. It uses a library like serde to parse the JSON and map it into a native Rust struct:

struct User {
    name: String,
    email: String,
}

Now the server has a proper, typed, compiled Rust data structure it can work with.

Step 5 — Business logic. The server validates the data, hashes a password, writes to a database — whatever the application requires.

Step 6 — Serialization (server side). The server builds a response as a native Rust struct, serializes it back into JSON, and sends the HTTP response:

{ "id": 42, "name": "Ali", "status": "created" }

Step 7 — Deserialization (client side). The frontend receives the JSON string, calls JSON.parse(), and gets a native JavaScript object to update the UI.

The entire dance: native → JSON → network → JSON → native. Both sides only ever deal with their own language and JSON. The network is invisible.


Beyond JSON: When Text Isn't Fast Enough

JSON is king for public-facing APIs. But it has a cost. It's verbose — every key name is repeated in every single object, and everything is text. A payload that's 90 bytes in JSON might be 30 bytes in a binary format.

When milliseconds and bandwidth matter — think microservice-to-microservice calls processing millions of requests, real-time data pipelines, or mobile apps on slow networks — engineers reach for binary serialization formats.

The Tradeoff Spectrum

Think of it as a slider between human-friendliness and machine-efficiency:

JSON — The default. Human-readable, universally supported, easy to debug. Use for public APIs, config files, logging, anything where a human might need to read the data.

Protocol Buffers (Protobuf) — Google's binary format. You define a schema (.proto file), and it generates serialization code for any language. Payloads are 3-10x smaller than JSON, parsing is significantly faster. The backbone of gRPC. Use for high-throughput internal services.

Apache Avro — A binary format with built-in schema evolution. The schema travels with the data (or lives in a Schema Registry). Heavily used in data pipelines, Kafka event streaming, and the Hadoop ecosystem. Use when schemas change frequently and backward compatibility is critical.

The Decision Rule

Ask yourself: "Will a human ever need to read this payload?"

  • Yes → JSON. Debuggability wins.
  • No, and performance matters → Binary format. Let machines talk to machines efficiently.

Most applications use JSON for external APIs and Protobuf/Avro for internal communication. You'll encounter both.


The Security Angle: Deserialization Is Where Danger Lives

This is something every backend engineer should have burned into memory.

Serialization is safe — you're converting your own data into a string. But deserialization is taking external input and reconstructing objects in your server's memory. That's inherently dangerous.

The OWASP Top 10 lists Insecure Deserialization as a critical vulnerability. Here's why:

Some serialization formats (Python's pickle, Java's ObjectInputStream, PHP's unserialize()) can reconstruct any object type — including ones that execute code during construction. An attacker who crafts a malicious serialized payload can achieve remote code execution on your server.

This is one reason JSON is inherently safer than native serialization formats. JSON only supports six primitive data types — no classes, no functions, no callbacks. You can't sneak executable code into a JSON string.

The rule: Never deserialize untrusted data using native serialization. Use JSON or a schema-validated binary format. Always validate after deserialization.


The Mental Model Summary

Carry this picture forward:

Serialization = packing your native data into a universal format for transit

Deserialization = unpacking that universal format into your native data on arrival

JSON = the universal format for ~80% of web communication (human-readable, safe, ubiquitous)

Binary formats (Protobuf, Avro) = the fast lane for machine-to-machine communication when performance matters

The network layers beneath are invisible to you — you deal with JSON in, JSON out. The OSI model handles the rest.

The concept is deceptively simple: agree on a common format, translate in, translate out. But this simple idea is what makes it possible for a JavaScript browser, a Rust API, a Python ML service, and a Go message queue to all work together seamlessly — without any of them knowing or caring what language the others speak.


This is part 3 of my "Backend from First Principles" series. Everything in this article comes from what I learned watching the Backend from First Principles playlist by Sriniously — specifically the video "Serialization and Deserialization for backend engineers." I'm not claiming original research. This is me taking what I studied, internalizing it, and presenting it as a mental model that made the concepts stick for me. If it helps you too, even better.

Playlist: https://youtube.com/playlist?list=PLui3EUkuMTPgZcV0QhQrOcwMPcBCcd_Q1

Follow along for the next one.

#BackendEngineering #Serialization #JSON #APIs #SoftwareEngineering #SystemDesign #LearningInPublic