Data Serialization and Persistence
In etchblok-test-api, domain models use a consistent pattern of to_dict and from_dict methods to bridge the gap between Python objects and JSON-serializable dictionaries. This approach ensures that complex types like datetime and Enum are correctly formatted for API responses and that incoming request data is properly mapped to model attributes.
Serializing Models for API Responses
When you need to return a model instance in a Flask response, use the to_dict() method. This method converts the model's internal state into a plain dictionary where all values are JSON-compatible (e.g., datetime objects become ISO 8601 strings and Enum members become their underlying values).
from flask import jsonify
from app.models.bookmark import Bookmark
def get_bookmark_response(bookmark: Bookmark):
# to_dict() handles ISO format dates and Enum values automatically
return jsonify(bookmark.to_dict())
Implementation Details
Each model implements to_dict() to include both stored attributes and calculated properties:
- Bookmark: Includes timestamps (
created_at,updated_at) as ISO strings and thestatusEnum as a string. - Tag: Includes the
colorEnum value and the currentusage_count. - Collection: Includes a calculated
sizeproperty and maps the internalcollection_typeattribute to atypekey in the dictionary.
Example from app/models/collection.py:
def to_dict(self) -> Dict[str, Any]:
"""Serialise to JSON-safe dictionary."""
return {
"id": self.id,
"name": self.name,
"type": self.collection_type.value, # Enum to string
"bookmark_ids": self.bookmark_ids,
"filter_rule": self.filter_rule,
"is_pinned": self.is_pinned,
"size": self.size, # Calculated property
"created_at": self.created_at.isoformat(), # Datetime to ISO string
}
Creating Models from Request Data
To instantiate a model from a JSON request body, use the @classmethod from_dict(). In etchblok-test-api, this method is primarily used in the service layer to create new instances from validated user input.
from app.models.tag import Tag
# Example request data
data = {"name": "Research", "color": "blue", "description": "Work related"}
# Create instance
new_tag = Tag.from_dict(data)
Usage in Service Layer
The BookmarkService uses from_dict after performing initial validation on the raw request data.
# app/services/bookmark_service.py
def create_bookmark(self, data: Dict[str, Any]) -> Tuple[Optional[Bookmark], Optional[str]]:
# 1. Validate raw data
error = _validate_url(data.get("url", "")) or _validate_title(data.get("title", ""))
if error:
return None, error
# 2. Instantiate model
bookmark = Bookmark.from_dict(data)
# 3. Persist
self._repo.save_bookmark(bookmark)
return bookmark, None
Handling Complex Types
The serialization methods in etchblok-test-api specifically manage the conversion of Enums and Datetimes to ensure consistency across the API.
Enums
Models like Tag and Collection use Enums for attributes like color and type. The from_dict methods handle the lookup of these Enums from string values.
# app/models/tag.py
@classmethod
def from_dict(cls, data: Dict[str, Any]) -> "Tag":
"""Construct a Tag from a dictionary."""
# Map string color to TagColor Enum, defaulting to GRAY
color = TagColor(data["color"]) if "color" in data else TagColor.GRAY
return cls(
name=data["name"],
color=color,
description=data.get("description", "")
)
Timestamps
The Bookmark and Collection models automatically generate created_at timestamps upon instantiation. While to_dict exports these as strings, from_dict typically ignores them during creation to allow the model to set its own initial state.
Troubleshooting and Gotchas
Partial State Restoration
The from_dict() methods in etchblok-test-api are designed for creation, not full state restoration. They typically only extract a subset of fields (like url and title) and do not restore internal fields like id, created_at, or updated_at from the input dictionary. If you need to restore a full object from a database, you must manually assign these fields or use a repository-specific loading mechanism.
Missing Required Fields
Bookmark.from_dict() and Tag.from_dict() will raise a KeyError if mandatory fields (like url or name) are missing from the input dictionary. Always validate the presence of required keys in the service layer before calling from_dict.
Field Name Mismatches
In some cases, the JSON key differs from the model attribute name. For example, Collection.from_dict() expects a type key in the dictionary but maps it to the collection_type attribute on the Collection instance.
# app/models/collection.py
@classmethod
def from_dict(cls, data: Dict[str, Any]) -> "Collection":
# Maps 'type' key to CollectionType enum
ctype = CollectionType(data.get("type", "manual"))
return cls(
name=data["name"],
collection_type=ctype,
filter_rule=data.get("filter_rule", ""),
)