Skip to content

Custom Serializers

Transform generated data into specialized formats like CSV, MessagePack, or other non-JSON request bodies.

When to use custom serializers

Use custom serializers when your API accepts structured data in formats that Schemathesis doesn't support by default:

  • Binary serialization - MessagePack, Protocol Buffers, or other compact formats
  • Data processing - CSV, TSV, or other delimited formats for bulk operations
  • Custom text formats - Specialized configuration files or domain-specific structures

Schemathesis generates data based on your JSON Schema but only supports common serialization formats out of the box.

Custom Serializers vs Media Types

Use custom serializers when you have a JSON Schema describing your data structure but need a different output format. Use custom media types when you need to generate raw data without a schema structure (like PDFs or images).

Quick Start: CSV Data Processing

OpenAPI Schema:

paths:
  /upload-users:
    post:
      requestBody:
        content:
          text/csv:
            schema:
              type: array
              items:
                type: object
                required: [first_name, last_name]
                properties:
                  first_name:
                    type: string
                    pattern: "^[A-Za-z]+$"
                  last_name:
                    type: string  
                    pattern: "^[A-Za-z]+$"

This schema tells Schemathesis to generate lists of dictionaries like:

[
    {"first_name": "John", "last_name": "Doe"},
    {"first_name": "Jane", "last_name": "Smith"}
]

Register a serializer that converts these dictionaries to CSV bytes:

# csv_serializer.py
import csv
import schemathesis
from io import StringIO

@schemathesis.serializer("text/csv")
def csv_serializer(ctx, value):
    """Convert list of dictionaries to CSV bytes"""
    # Handle binary data from external examples
    if isinstance(value, bytes):
        return value

    # Handle unexpected types in negative testing  
    if not isinstance(value, list) or \
      not all(isinstance(item, dict) for item in value):
        return str(value).encode('utf-8')

    if not value:
        return b""  # Empty CSV

    # Convert dictionaries to CSV
    output = StringIO()
    field_names = sorted(value[0].keys()) if value else []
    writer = csv.DictWriter(output, field_names)
    writer.writeheader()
    writer.writerows(value)

    return output.getvalue().encode('utf-8')  # Must return bytes or None
export SCHEMATHESIS_HOOKS=csv_serializer
schemathesis run http://localhost:8000/openapi.json

Result: Your /upload-users endpoint receives properly formatted CSV data instead of JSON.

Skipping Serialization

If your serializer returns None, the resulting request will have no body.

Essential Patterns

Multiple aliases for the same format

@schemathesis.serializer(
    "text/csv", "text/comma-separated-values", "application/csv"
)
def csv_serializer(ctx, value):
    # Same implementation handles all aliases
    if isinstance(value, bytes):
        return value
    return serialize_to_csv(value)  # Returns bytes

Context-aware serialization

@schemathesis.serializer("text/csv")
def context_aware_csv(ctx, value):
    """Use test case information to customize serialization"""
    if isinstance(value, bytes):
        return value

    # Different CSV format based on endpoint
    if "/bulk-import" in ctx.case.path:
        delimiter = '\t'  # Use tabs for bulk import
    else:
        delimiter = ','   # Use commas for regular upload

    return serialize_csv_with_delimiter(value, delimiter).encode('utf-8')

Automatic Transport Registration

Serializers are automatically registered for all transport types (HTTP requests, ASGI, WSGI)

What's Next