StackPractices
beginner By Mathias Paulenko

Serialize and Deserialize Data

How to serialize and deserialize data in JSON, XML, and YAML across Python, Java, and JavaScript.

Topics: data

Overview

Serialization turns an in-memory object into a storable, transmittable format; deserialization reverses the process and rebuilds the object from bytes or text. You’ll hit both everywhere: APIs, caches, message queues, config files, session persistence. Working snippets for JSON, XML and YAML in Python, JavaScript and Java come right after this. If you need flat key-value pairs instead of a serialized byte stream, see Flatten and Unflatten Nested Objects.

When to Use

Reach for serialization when you need to:

  • Ship payloads across HTTP APIs or a message broker
  • Persist app state on disk or in a cache layer
  • Move a config between formats (JSON ↔ YAML ↔ XML)
  • Pass typed messages between services that share no codebase

Solution

Python

import json

# Serialize (object -> JSON string)
data = {'name': 'Alice', 'age': 30, 'active': True}
json_str = json.dumps(data, indent=2)
print(json_str)

# Deserialize (JSON string -> object)
parsed = json.loads(json_str)
print(parsed['name'])
# YAML serialization with PyYAML
import yaml

yaml_str = yaml.safe_dump(data, default_flow_style=False)
parsed_yaml = yaml.safe_load(yaml_str)

JavaScript

// JSON is native to JavaScript
const data = { name: 'Alice', age: 30, active: true };

// Serialize
const jsonStr = JSON.stringify(data, null, 2);
console.log(jsonStr);

// Deserialize
const parsed = JSON.parse(jsonStr);
console.log(parsed.name);
// YAML serialization with js-yaml
// npm install js-yaml
import yaml from 'js-yaml';

const yamlStr = yaml.dump(data);
const parsedYaml = yaml.load(yamlStr);

Java

// Jackson is the standard for JSON in Java
// Maven: com.fasterxml.jackson.core:jackson-databind
import com.fasterxml.jackson.databind.ObjectMapper;

public class SerializationDemo {
    public static void main(String[] args) throws Exception {
        ObjectMapper mapper = new ObjectMapper();

        User user = new User("Alice", 30, true);

        // Serialize
        String json = mapper.writerWithDefaultPrettyPrinter().writeValueAsString(user);
        System.out.println(json);

        // Deserialize
        User parsed = mapper.readValue(json, User.class);
        System.out.println(parsed.getName());
    }
}

class User {
    private String name;
    private int age;
    private boolean active;

    public User() {}
    public User(String name, int age, boolean active) {
        this.name = name; this.age = age; this.active = active;
    }
    public String getName() { return name; }
    public void setName(String name) { this.name = name; }
    public int getAge() { return age; }
    public void setAge(int age) { this.age = age; }
    public boolean isActive() { return active; }
    public void setActive(boolean active) { this.active = active; }
}

Explanation

Mermaid flowchart LR diagram

JSON is the dominant interchange format due to its simplicity and native support in JavaScript and modern languages. It maps cleanly to dictionaries/objects, arrays, strings, numbers, booleans, and null. XML remains relevant in enterprise SOAP services, configuration files (Spring, Android), and document-centric workflows. YAML is preferred for human-edited configs because it supports comments and complex nesting with minimal syntax.

Under the hood, Jackson reflects on your POJO — it binds JSON fields through getters/setters or, if you let it, public fields. Python json works with any JSON-serializable type (dicts, lists, primitives). JavaScript JSON.stringify handles cycles poorly (throws TypeError) unless a replacer is provided.

Variants

TechnologyFormatLibraryApproachNotes
PythonJSONjson (stdlib)dumps() / loads()Handles basic types, custom encoder support
PythonYAMLPyYAMLsafe_dump() / safe_load()Supports custom tags and anchors
PythonXMLxml.etree.ElementTreetostring() / fromstring()Standard library, no schema validation
JavaScriptJSONNativeJSON.stringify() / JSON.parse()Zero dependencies, supports reviver/replacer
JavaScriptYAMLjs-yamldump() / load()Fast, safe by default
JavaJSONJacksonwriteValueAsString() / readValue()POJO binding, streaming, tree model
JavaXMLJAXB / Jackson XMLAnnotation-drivenJAXB is deprecated; prefer Jackson XML

Text formats (JSON, XML, YAML) are self-describing and debuggable; binary formats (MessagePack, Protocol Buffers, Avro) are smaller and faster to parse but need shared schemas and tooling to inspect. Pick schema-first formats when services owned by different teams exchange data — the contract lives outside the code — and schemaless JSON when flexibility matters more than payload size.

What Works

  • Use safe_load for YAML in untrusted contexts to prevent arbitrary code execution
  • Validate against a schema after deserialization to catch structural errors before business logic — see Validate JSON Schema
  • Handle circular references explicitly in JSON.stringify with a replacer or a library like flatted
  • Version your serialized data by adding a schema_version field for backward-compatible evolution
  • Prefer JSON for APIs and YAML for configs; avoid XML unless integrating with legacy systems

Common Mistakes

  • Not handling undefined in JavaScript: JSON.stringify silently drops undefined values, functions, and Symbol-keyed properties. A field present in memory can vanish from the wire payload without any error.
  • Forgetting default constructors in Java: Jackson requires a no-arg constructor for deserialization
  • Using float for monetary values: serialization can introduce precision errors; use Decimal / BigDecimal
  • Not setting content-type headers: APIs should send application/json, not text/plain
  • Ignoring encoding issues: always specify UTF-8 when reading and writing serialized text files
  • Loading YAML with an unsafe loader: yaml.load() in PyYAML can instantiate arbitrary objects; use safe_load on anything you don’t fully trust
  • Assuming JSON preserves key order and exact numbers: objects are unordered, and JavaScript loses integer precision beyond 2^53 — use strings or BigInt for large IDs

When Not to Use This Approach

  • Binary payloads over internal links: if payload size is the bottleneck between your own services, a binary format like MessagePack or Protobuf beats text formats.
  • Strongly-typed contracts across teams: when producers and consumers evolve independently, schemaless JSON drifts silently; adopt a schema evolution policy and contract tests instead.
  • Streaming multi-gigabyte payloads: loading the whole document into memory fails at scale; switch to streaming parsers (Jackson JsonParser, Python ijson) or a columnar format.
  • Persisting sensitive objects directly: serializing an entity dumps the whole object graph, including fields you may not want stored; serialize dedicated DTOs instead.

Security Considerations

  • Native object formats on untrusted input are RCE: pickle.loads(), Java native serialization and PHP unserialize() run whatever the byte stream tells them to. Anything crossing your trust boundary goes through a data-only format — JSON, not pickle.
  • Prototype pollution via JSON merge: merging user-supplied JSON containing __proto__ or constructor keys can pollute JavaScript object prototypes. Strip dangerous keys or build the result on a null-prototype object.
  • Unsafe YAML loaders: yaml.load() (PyYAML) and equivalent unsafe loaders construct arbitrary objects from the document, so keep safe_load/SafeLoader on anything that crosses your trust boundary.
  • XXE in XML parsers: a crafted external-entity declaration lets the parser read local files or hit internal endpoints. Turn off DTDs and external entities — FEATURE_SECURE_PROCESSING under Java, defusedxml under Python.
  • Duplicate-key ambiguity: feed {"role": "user", "role": "admin"} to two different parsers and you can get two different results — last value wins for most, first for a few. That gap is exploitable: a WAF reading the raw text may see one thing while your parser sees another.
  • Depth and size limits: a document nested 10,000 levels deep, or a string spanning hundreds of MB, will blow the stack or exhaust memory. Cap both at the boundary.

Performance Considerations

  • stdlib vs optimized parsers: Python’s json covers most workloads; orjson runs several times faster on high-throughput services and hands you bytes back. In JavaScript, JSON.parse/JSON.stringify already run on V8’s fast path.
  • Pretty-printing costs size: indent=2 inflates payloads roughly 20–40%. Use it for configs humans read, not for wire formats.
  • Binding vs streaming: Jackson’s ObjectMapper allocates the full object graph; the streaming JsonParser API reads very large documents with nearly constant memory.
  • Binary formats when size matters: MessagePack and Protobuf payloads are typically several times smaller than equivalent JSON and parse faster — the trade-off is losing human readability and needing shared schemas.
  • Cache serialized bytes: on hot paths serving immutable payloads, serialize once and keep the bytes — re-serializing the same object per request is wasted CPU.

Testing Strategy

  • Round-trip tests: serialize, then deserialize, and assert structural equality. This catches silent type coercion — dates become strings, tuples become lists, Decimal becomes float.
  • Edge-case corpus: {}, null, deeply nested arrays, Unicode strings (emoji, RTL), and integers past 2^53 — JavaScript silently rounds those.
  • Type coverage: walk your schema and verify every field type survives the round-trip. datetime, Decimal, bytes, and enums all need explicit encoders in Python’s json or custom serializers in Jackson.
  • Adversarial input: deeply nested JSON (thousands of levels), very large strings, tens of thousands of keys, and duplicate keys to probe parser limits.
  • Contract tests across versions: one fixture per released schema version, all of them deserialized in CI. Breaking changes surface before deploy instead of in production.

Error Handling and Recovery

  • Malformed input: catch parse errors at the boundary and return a structured error with the offending location (json.JSONDecodeError in Python, SyntaxError in JS, JsonParseException in Jackson) — never a bare 500.
  • Encoding mismatches: decode explicitly as UTF-8 and surface UnicodeDecodeError early. A file saved as UTF-16 or carrying a BOM fails in confusing ways when read as plain UTF-8.
  • Schema/version mismatch: a payload with no schema_version, or one newer than you support, deserves a clear error and a migration path — not silently dropped fields.
  • Partial deserialization: a payload can be valid JSON and still miss required fields. Validate after parsing, not instead of it.

Troubleshooting

  • Object of type X is not JSON serializable (Python): the type has no default encoder — pass a default= callable or subclass JSONEncoder for datetime, Decimal, set, and bytes.
  • TypeError: Converting circular structure to JSON (JavaScript): the object graph has cycles — use a replacer that skips back-references or a library like flatted.
  • Jackson fails on unknown fields: either add the fields to the POJO or configure FAIL_ON_UNKNOWN_PROPERTIES to false for forward compatibility.
  • Garbled accents or à characters in output: the file was written in one encoding and read in another. Specify UTF-8 on both ends and handle BOMs explicitly.
  • Dates come back as strings: JSON has no date type. Agree on ISO-8601 strings and parse them explicitly, or each side ends up with a different type.

Further Reading

Frequently Asked Questions

Which serialization format should I choose for microservices?

Inside your own fleet, Protobuf or MessagePack wins on payload size and typed contracts. For public APIs and anything humans read, JSON stays the right call — self-describing and understood everywhere.

How do I handle custom object serialization in Python?

Two options: subclass JSONEncoder, or hand json.dumps() a default= callable that returns a serializable dict for your type. On the way back, an object_hook passed to json.loads() turns those dicts into your objects again.

Can I serialize Java objects without getters and setters?

Yes. Jackson can serialize public fields directly if configured with ObjectMapper.setVisibility(PropertyAccessor.FIELD, Visibility.ANY). However, using getters/setters is the standard Java convention and keeps encapsulation. Alternatively, use records (Java 14+) which generate canonical constructors and accessor methods automatically.

Why does JSON.stringify silently drop fields in JavaScript?

undefined values, functions, and Symbol-keyed properties are removed rather than serialized. Date objects become ISO strings and Map/Set serialize to {}. Either preprocess the object first or diff the field list against the original after stringify.

Is it safe to unpickle data from an untrusted source?

No. A pickle isn't data, it's an instruction stream — pickle.loads() runs whatever opcodes are in there, so an attacker-crafted pickle is effectively a shell on your machine. Only unpickle bytes you produced yourself; anything external goes through JSON.