pyguides

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 provided
  • kw_only: Field must be passed as keyword argument
  • omit_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] or T | 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