Data Serialization Patterns
To handle JSON data exchange for API integration in this project, use the to_dict and from_dict methods provided by the domain models. These methods serve as the bridge between internal Python objects and the external JSON-safe dictionaries used by Flask's jsonify and request.get_json().
Basic Integration Pattern
The standard pattern involves receiving JSON in a route, passing it to a service that uses from_dict to create an instance, and then returning the result of to_dict back to the client.
from flask import request, jsonify
from app.services.bookmark_service import BookmarkService
_service = BookmarkService()
@bookmarks_bp.route("/", methods=["POST"])
def create_bookmark():
# 1. Get raw JSON from request
data = request.get_json(force=True)
# 2. Service uses Model.from_dict(data) internally
bookmark, error = _service.create_bookmark(data)
if error:
return jsonify({"error": error}), 400
# 3. Convert model back to JSON-safe dict for response
return jsonify(bookmark.to_dict()), 201
Serializing Models to JSON (to_dict)
The to_dict method converts complex Python types (like Enums and Datetimes) into JSON-compatible primitives.
Bookmark Serialization
The Bookmark.to_dict method handles ISO-8601 formatting for timestamps and extracts the string value from the BookmarkStatus enum.
# app/models/bookmark.py
def to_dict(self) -> Dict[str, Any]:
return {
"id": self.id,
"url": self.url,
"title": self.title,
"description": self.description,
"tags": self.tags,
"status": self.status.value, # Enum to string
"created_at": self.created_at.isoformat(), # Datetime to ISO string
"updated_at": self.updated_at.isoformat(),
"metadata": self.metadata,
}
Collection Serialization with Calculated Fields
The Collection.to_dict method includes a calculated size property that is not a stored attribute but is useful for the API consumer.
# app/models/collection.py
def to_dict(self) -> Dict[str, Any]:
return {
"id": self.id,
"name": self.name,
"type": self.collection_type.value,
"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(),
}
Deserializing JSON to Models (from_dict)
The from_dict class method instantiates a model from a dictionary. It typically only maps user-provided fields, allowing the class constructor to generate system fields like id and created_at.
Creating a Bookmark
When creating a Bookmark, only the core content fields are extracted from the input.
# app/models/bookmark.py
@classmethod
def from_dict(cls, data: Dict[str, Any]) -> "Bookmark":
return cls(
url=data["url"],
title=data["title"],
description=data.get("description", ""),
tags=data.get("tags", []),
)
Creating a Tag with Enum Mapping
The Tag.from_dict method includes logic to map a string color name back to the TagColor enum.
# app/models/tag.py
@classmethod
def from_dict(cls, data: Dict[str, Any]) -> "Tag":
# Map string to Enum, defaulting to GRAY if missing
color = TagColor(data["color"]) if "color" in data else TagColor.GRAY
return cls(
name=data["name"],
color=color,
description=data.get("description", "")
)
Troubleshooting and Gotchas
System Fields are Ignored in from_dict
The from_dict methods in this codebase are designed for creation, not full state restoration. Fields like id, status, created_at, and updated_at are ignored even if present in the input dictionary. These are generated automatically by the dataclass field factories:
# These fields are NOT populated by from_dict
id: str = field(default_factory=lambda: uuid.uuid4().hex[:12])
created_at: datetime = field(default_factory=datetime.utcnow)
Enum Validation
The from_dict methods for Tag and Collection pass raw strings directly into Enum constructors (e.g., TagColor(data["color"])). If the input string does not match a valid Enum member, a ValueError will be raised. Ensure data is validated before calling from_dict or handle the exception in the service layer.
Missing Required Fields
Bookmark.from_dict accesses data["url"] and data["title"] directly. If these keys are missing from the API payload, it will raise a KeyError. The BookmarkService typically validates these fields before calling the model method:
# app/services/bookmark_service.py
def create_bookmark(self, data: Dict[str, Any]):
# Validation happens BEFORE from_dict
error = _validate_url(data.get("url", "")) or _validate_title(data.get("title", ""))
if error:
return None, error
bookmark = Bookmark.from_dict(data)
# ...