Fast Serialisation with msgspec
msgspec is a fast serialisation library for Python that combines correctness with raw speed. It pairs the performance of binary formats like MessagePack with the schema validation of tools like Pydantic, but with a fraction of the overhead. If your application spends significant time encoding and decoding JSON or MessagePack payloads, swapping the standard library’s json module for msgspec is one of the simplest performance wins available.
Why msgspec?
If you work with JSON, MessagePack, or other serialization formats in Python, you have likely encountered performance bottlenecks or runtime validation errors. msgspec addresses both:
- Zero-copy decoding: Messages are decoded directly into Python objects without intermediate representations
- Schema validation: Define expected structures once and get automatic validation
- Native binary support: Use MessagePack for compact, fast serialization
- Minimal dependencies: Single package with no required runtime dependencies
Installation
pip install msgspec
Basic Usage
msgspec revolves around a central idea: you define your data shapes as Python classes that inherit from msgspec.Struct, and the library handles encoding and decoding automatically. Unlike Pydantic, which validates at object creation time, msgspec validates during decoding, catching malformed input before it ever becomes an object in your application.
Structs
Define your data structures using msgspec.Struct:
import msgspec
class User(msgspec.Struct):
name: str
email: str
age: int
# Create an instance
user = User(name="Alice", email="alice@example.com", age=30)
# Encode to JSON
json_data = msgspec.json.encode(user)
# b'{"name":"Alice","email":"alice@example.com","age":30}'
# Decode from JSON
decoded = msgspec.json.decode(json_data, type=User)
# User(name='Alice', email='alice@example.com', age=30)
The Struct above shows JSON encoding and decoding in a few lines of code. The same User struct works with MessagePack without any changes to the class definition; you only replace the encoder and decoder calls. MessagePack produces a compact binary representation that is smaller than JSON for most payloads, making it a better choice when bandwidth or storage is tight. The type annotation on the decode call tells msgspec which struct to reconstruct, so you get back a fully typed User instance rather than a generic dictionary.
MessagePack
For more compact storage, use MessagePack:
import msgspec
# Encode to MessagePack
msgpack_data = msgspec.msgpack.encode(user)
# b'\x83\xa4name\xa5Alice\xa5email\xbbalice@example.com\xa3age\x1e'
# Decode from MessagePack
decoded = msgspec.msgpack.decode(msgpack_data, type=User)
MessagePack typically produces smaller payloads than JSON, which is useful for network transmission or storage-constrained environments.
Beyond raw size, MessagePack encodes typed data natively: integers, floats, and strings each have their own wire format, which avoids the ambiguity JSON suffers from. If you are building an internal service mesh where every endpoint speaks MessagePack, msgspec handles both the encoding and the struct reconstruction in one pass without intermediate dict representations.
Schema Validation
One of msgspec’s strongest features is built-in validation at decode time:
import msgspec
class User(msgspec.Struct):
name: str
email: str
age: int
# Valid data - succeeds
valid_json = b'{"name":"Alice","email":"alice@example.com","age":30}'
user = msgspec.json.decode(valid_json, type=User)
# Invalid data - raises ValidationError
invalid_json = b'{"name":"Alice","email":"not-an-email","age":"thirty"}'
try:
user = msgspec.json.decode(invalid_json, type=User)
except msgspec.ValidationError as e:
print(f"Validation failed: {e}")
This means malformed data is caught immediately, not later in your application when it causes harder-to-diagnose bugs.
The validation shown here is structural: it checks that each field has the declared type. For more complex rules, you can implement custom validators or combine msgspec with a lightweight validation layer. The benefit of keeping validation at decode time is that you never hold an invalid object in memory; the bad data is rejected before the struct is constructed. Once you have validated decoding working for flat structs, the natural next step is nesting structs inside each other.
Nested Structures
msgspec handles nested structs naturally:
import msgspec
class Address(msgspec.Struct):
street: str
city: str
country: str
class Person(msgspec.Struct):
name: str
address: Address
emails: list[str]
person = Person(
name="Bob",
address=Address(street="123 Main St", city="London", country="UK"),
emails=["bob@example.com", "bob.work@example.com"]
)
# Encode and decode
json_data = msgspec.json.encode(person)
decoded = msgspec.json.decode(json_data, type=Person)
Field Options
Nested structs define the shape of your data, but real-world schemas also need optional fields, defaults, and control over encoding behaviour. msgspec provides field options that let you tune how individual fields behave without changing the struct’s public interface.
Control how fields are handled with options:
import msgspec
class Config(msgspec.Struct):
api_key: str # Required
timeout: int = 30 # Optional with default
debug: bool = msgspec.field(default=False, kw_only=True)
# Use keyword arguments for optional fields
config = Config(api_key="secret", timeout=60)
Available field options:
default: Default value if not providedkw_only: Field must be passed as keyword argumentomit_defaults: Skip encoding fields with default values
Field options give you fine-grained control over struct behaviour, but the real power of msgspec comes from its support for Python’s type system. The same struct definitions work with standard type annotations, making the transition from type-checked code to validated serialization straightforward.
Type Annotations
msgspec supports most common type annotations:
from typing import Optional, List, Dict
import msgspec
class Event(msgspec.Struct):
id: str
name: str
tags: list[str]
metadata: dict[str, str]
priority: Optional[int] = None
participants: list[str] = msgspec.field(default_factory=list)
Supported types:
- Primitives:
str,int,float,bool,bytes - Collections:
list[T],dict[K, V],set[T] - Optional:
Optional[T]orT | None - Union:
Union[A, B](decoded to first matching type) - Nested structs
The breadth of type support means you can model most API contracts with msgspec structs alone. When you do need custom logic, you can subclass and override the default encoding and decoding hooks. Now that the type system is clear, the question becomes: how much faster is this than the alternatives?
Performance Comparison
msgspec consistently outperforms other serialization libraries:
import json
import msgspec
import msgspec.jsonb as jsonb
# Test data
data = {"users": [{"name": f"User{i}", "age": i % 100} for i in range(1000)]}
# JSON (stdlib)
json_encoded = json.dumps(data)
json_decoded = json.loads(json_encoded)
# msgspec JSON
msgspec_encoded = msgspec.json.encode(data)
msgspec_decoded = msgspec.json.decode(msgspec_encoded)
# msgspec JSONB (faster, binary JSON variant)
jsonb_encoded = jsonb.encode(data)
jsonb_decoded = jsonb.decode(jsonb_encoded)
Typical results show msgspec is 2-5x faster than stdlib json for both encoding and decoding, with JSONB providing another significant boost.
The speed comes from msgspec’s design: it parses directly into pre-allocated struct instances rather than building intermediate dict and list objects. For APIs that process thousands of requests per second, this difference translates to lower latency and reduced CPU costs. With the performance baseline established, the patterns below show how msgspec fits into common application scenarios.
Common Patterns
Date and Time
from datetime import datetime, date
import msgspec
class Event(msgspec.Struct):
name: str
start_date: datetime
created_date: date
event = Event(
name="Conference",
start_date=datetime(2026, 3, 20, 9, 0),
created_date=date.today()
)
Enum Support
msgspec handles datetime objects natively, serialising them to ISO 8601 strings and parsing them back without extra configuration. The same built-in integration extends to Python enumerations, which are encoded as their values and reconstructed by matching against the enum members.
from enum import Enum
import msgspec
class Status(Enum):
PENDING = "pending"
ACTIVE = "active"
COMPLETED = "completed"
class Task(msgspec.Struct):
name: str
status: Status
task = Task(name="Build feature", status=Status.ACTIVE)
encoded = msgspec.json.encode(task)
When to Use msgspec
Choose msgspec when you need:
- High-throughput serialization (APIs, data pipelines)
- Schema validation without the overhead of full validation frameworks
- Compact binary representation with MessagePack
- Zero-copy decoding for large messages
Stick with JSON or Pydantic when:
- You need extensive validation beyond type checking
- Schema evolution is complex
- You need JSON Schema generation
- Your team is already invested in other tools
See Also
- msgspec documentation : Official library documentation
- MessagePack format : Binary serialization format
jsonmodule : Python’s built-in JSON handling- dataclasses : Built-in Python class definitions