Encoders¶
Encoders let you serialize and deserialize custom data types in Ravyn. Want to use attrs, msgspec, or your own data classes? Encoders make it possible without framework limitations.
What You'll Learn¶
- What encoders are and why they're useful
- Creating custom encoders
- Registering encoders in your app
- Using encoders with Pydantic and MsgSpec
- Building encoders for attrs and other libraries
Quick Start¶
from ravyn import Ravyn
from ravyn.encoders import Encoder
from pydantic import BaseModel
# Pydantic works out of the box
class User(BaseModel):
name: str
email: str
@app.get("/user")
def get_user() -> User:
return User(name="Alice", email="alice@example.com")
# Automatically serialized to JSON!
app = Ravyn()
app.add_route(get_user)
Ravyn automatically handles Pydantic models and MsgSpec structs. For other types, create custom encoders.
Why Use Encoders?¶
Benefits:¶
-
Framework Independence - Use any serialization library
-
Future Proof - Add support for new libraries anytime
-
Type Safety - Automatic validation and serialization
-
Flexibility - Full control over encoding/decoding
Built-In Support:¶
- Pydantic models
- MsgSpec structs
- Python dataclasses
- Standard Python types
How Encoders Work¶
An encoder has three responsibilities:
is_type()- Check if an object is the right typeserialize()- Convert object to JSON-serializable formatencode()- Convert JSON data to object
from ravyn.encoders import Encoder
class MyEncoder(Encoder):
def is_type(self, value):
# Check if value is the right type
pass
def serialize(self, obj):
# Convert obj to dict/JSON
pass
def encode(self, data, target_type):
# Convert dict/JSON to obj
pass
Creating Custom Encoders¶
Example: attrs Library¶
from ravyn.encoders import Encoder
import attrs
class AttrsEncoder(Encoder):
def is_type(self, value):
# attrs uses decorators, check with has()
return attrs.has(value)
def serialize(self, obj):
# Convert attrs instance to dict
return attrs.asdict(obj)
def encode(self, data, target_type):
# Convert dict to attrs instance
if self.is_type(target_type):
return target_type(**data)
return data
Using the Custom Encoder¶
from ravyn import Ravyn, get
import attrs
@attrs.define
class Product:
name: str
price: float
# Register encoder
app = Ravyn(encoders=[AttrsEncoder])
@get("/product")
def get_product() -> Product:
return Product(name="Laptop", price=999.99)
# Automatically serialized!
Built-In Encoders¶
Pydantic Encoder¶
Ravyn includes Pydantic support by default:
from pydantic import BaseModel
class User(BaseModel):
name: str
age: int
@app.get("/user")
def get_user() -> User:
return User(name="Alice", age=30)
# Works automatically!
How it works:
# Simplified version of Pydantic encoder
class PydanticEncoder(Encoder):
def is_type(self, value):
from pydantic import BaseModel
return isinstance(value, type) and issubclass(value, BaseModel)
def serialize(self, obj):
return obj.model_dump()
def encode(self, data, target_type):
return target_type(**data)
MsgSpec Encoder¶
Also built-in:
from msgspec import Struct
class Product(Struct):
name: str
price: float
@app.get("/product")
def get_product() -> Product:
return Product(name="Phone", price=699.99)
# Works automatically!
Registering Encoders¶
Via Application Instance¶
from ravyn import Ravyn
app = Ravyn(encoders=[AttrsEncoder, MyCustomEncoder])
Via Settings¶
from ravyn import RavynSettings
class Settings(RavynSettings):
@property
def encoders(self):
return [
"myapp.encoders.AttrsEncoder",
"myapp.encoders.CustomEncoder"
]
app = Ravyn() # Encoders loaded from settings
Via add_encoder() Method¶
from ravyn import Ravyn
app = Ravyn()
app.add_encoder(AttrsEncoder)
Warning
Use add_encoder() carefully. Encoders should be registered before defining routes that use them.
Complete Custom Encoder Example¶
Dataclass Encoder¶
from ravyn.encoders import Encoder
from dataclasses import is_dataclass, asdict
class DataclassEncoder(Encoder):
def is_type(self, value):
"""Check if value is a dataclass"""
return is_dataclass(value)
def serialize(self, obj):
"""Convert dataclass to dict"""
return asdict(obj)
def encode(self, data, target_type):
"""Convert dict to dataclass"""
if self.is_type(target_type):
return target_type(**data)
return data
Using It¶
from ravyn import Ravyn, get
from dataclasses import dataclass
@dataclass
class Book:
title: str
author: str
pages: int
app = Ravyn(encoders=[DataclassEncoder])
@get("/book")
def get_book() -> Book:
return Book(
title="Python Guide",
author="Alice",
pages=350
)
# GET /book
# Returns: {"title": "Python Guide", "author": "Alice", "pages": 350}
Advanced Encoder Example¶
JSON Encoder with Custom Types¶
from ravyn.encoders import Encoder
from datetime import datetime
from decimal import Decimal
class CustomJSONEncoder(Encoder):
def is_type(self, value):
# Support datetime and Decimal
return isinstance(value, (datetime, Decimal))
def serialize(self, obj):
if isinstance(obj, datetime):
return obj.isoformat()
elif isinstance(obj, Decimal):
return float(obj)
return obj
def encode(self, data, target_type):
if target_type == datetime:
return datetime.fromisoformat(data)
elif target_type == Decimal:
return Decimal(str(data))
return data
Common Pitfalls & Fixes¶
Pitfall 1: Not Implementing All Methods¶
Problem: Missing required methods.
# Wrong - missing serialize and encode
class BadEncoder(Encoder):
def is_type(self, value):
return isinstance(value, MyType)
Solution: Implement all three methods:
# Correct
class GoodEncoder(Encoder):
def is_type(self, value):
return isinstance(value, MyType)
def serialize(self, obj):
return obj.to_dict()
def encode(self, data, target_type):
return target_type.from_dict(data)
Pitfall 2: Registering Encoder Too Late¶
Problem: Routes defined before encoder registered.
# Wrong - route defined before encoder
@app.get("/data")
def get_data() -> MyType:
return MyType()
app.add_encoder(MyTypeEncoder) # Too late!
Solution: Register encoders before defining routes:
# Correct
app = Ravyn(encoders=[MyTypeEncoder])
@app.get("/data")
def get_data() -> MyType:
return MyType()
Pitfall 3: Incorrect is_type() Check¶
Problem: is_type() doesn't properly identify the type.
# Wrong - checks instance instead of type
class BadEncoder(Encoder):
def is_type(self, value):
return isinstance(value, MyClass) # Wrong for type checking
Solution: Check if it's a class and subclass:
# Correct
class GoodEncoder(Encoder):
def is_type(self, value):
return isinstance(value, type) and issubclass(value, MyClass)
Pitfall 4: Not Handling Nested Objects¶
Problem: Encoder doesn't serialize nested objects.
# Wrong - nested objects not handled
class BadEncoder(Encoder):
def serialize(self, obj):
return {"name": obj.name} # Ignores nested objects
Solution: Recursively serialize nested objects:
# Correct
class GoodEncoder(Encoder):
def serialize(self, obj):
return {
"name": obj.name,
"nested": obj.nested.to_dict() if obj.nested else None
}
Encoder Patterns¶
Pattern 1: Library-Specific Encoder¶
# For attrs
class AttrsEncoder(Encoder):
def is_type(self, value):
import attrs
return attrs.has(value)
def serialize(self, obj):
import attrs
return attrs.asdict(obj)
def encode(self, data, target_type):
if self.is_type(target_type):
return target_type(**data)
return data
Pattern 2: Multiple Type Support¶
class MultiTypeEncoder(Encoder):
def is_type(self, value):
return isinstance(value, (TypeA, TypeB, TypeC))
def serialize(self, obj):
if isinstance(obj, TypeA):
return obj.to_dict_a()
elif isinstance(obj, TypeB):
return obj.to_dict_b()
else:
return obj.to_dict_c()
def encode(self, data, target_type):
if target_type == TypeA:
return TypeA.from_dict(data)
elif target_type == TypeB:
return TypeB.from_dict(data)
else:
return TypeC.from_dict(data)
Best Practices¶
1. Test Your Encoders¶
def test_encoder():
encoder = MyEncoder()
# Test is_type
assert encoder.is_type(MyClass)
# Test serialize
obj = MyClass(name="test")
data = encoder.serialize(obj)
assert data == {"name": "test"}
# Test encode
new_obj = encoder.encode(data, MyClass)
assert new_obj.name == "test"
2. Handle Edge Cases¶
class RobustEncoder(Encoder):
def serialize(self, obj):
try:
return obj.to_dict()
except AttributeError:
# Fallback for objects without to_dict()
return {"value": str(obj)}
3. Document Your Encoders¶
class MyEncoder(Encoder):
"""
Encoder for MyCustomType.
Supports:
- Serialization to JSON
- Deserialization from JSON
- Nested object handling
"""
pass
Next Steps¶
Now that you understand encoders, explore:
- Responses - Return data to clients
- Requests - Parse incoming data
- Dependencies - Inject dependencies
- Application Settings - Configure encoders