Serialization
The ModelSerializer Interface
Section titled “The ModelSerializer Interface”Each protocol policy defines a serializer that converts between internal JSON and the protocol’s wire format:
interface ModelSerializer { // Model metadata ModelDescription definition();
// Response formatting (returns streaming string ranges, not Json) InputRange!string toResponseList(InputRange!Json data, OperationContext ctx); InputRange!string toResponseItem(Json data, OperationContext ctx);
// Request parsing Json fromRequest(string id, Json data);
// OpenAPI schema keys and definitions string respondItemSchemaKey(); string respondListSchemaKey(); string itemKey(); Schema[string] schemas();}The serializer sits between the database layer (which works with raw JSON) and the HTTP response. It returns InputRange!string for streaming — chunks are written to the response as they’re produced, avoiding full materialization in memory. Different protocols format the same data differently.
REST API Serialization
Section titled “REST API Serialization”RestApiSerializer wraps data in a key matching the model name:
Single Item Response
Section titled “Single Item Response”Conceptually, the serializer wraps the item in a key matching the model’s singular name:
Input: {"_id": "abc", "name": "Alice"}
Output:
{ "user": { "_id": "abc", "name": "Alice" }}List Response
Section titled “List Response”The list serializer wraps items in the plural key. Items are streamed — the response mapper runs on each item as it’s produced:
Output:
{ "users": [ {"_id": "abc", "name": "Alice"}, {"_id": "def", "name": "Bob"} ]}Request Parsing
Section titled “Request Parsing”REST requests wrap the data in a model key:
{ "user": { "name": "Charlie", "email": "charlie@example.com" }}The serializer extracts the inner object using the item ID and request body:
Json fromRequest(string id, Json data) { return data[definition.singular];}Model Description
Section titled “Model Description”The ModelDescription struct provides metadata about a model, computed at compile time via describeModel!T:
struct ModelDescription { string singular; // "user" string plural; // "users" Field[] fields; // Field names, types, optionality}Name Generation
Section titled “Name Generation”The singular name is derived from the struct name:
User→userMapFile→mapFileSiteSubset→siteSubset
The plural name appends “s”:
user→usersmapFile→mapFiles
Field Introspection
Section titled “Field Introspection”Each field records:
- Name
- D type
- Whether it’s
@optional - Whether it has a custom serializer
- Whether it’s a relation (references another model with
_id)
D Struct to JSON
Section titled “D Struct to JSON”vibe.d’s serializeToJson handles the base conversion from D structs to JSON:
struct Point { string type = "Point"; float[2] coordinates;}
auto p = Point("Point", [1.0, 2.0]);auto json = p.serializeToJson;// {"type": "Point", "coordinates": [1.0, 2.0]}Custom Serialization
Section titled “Custom Serialization”Models can override the default serialization by implementing toJson and fromJson:
struct Site { string _id; Point position;
Json toJson() const @safe { Json data = Json.emptyObject; data["_id"] = _id; data["position"] = position.serializeToJson; return data; }
static Site fromJson(Json src) @safe { Site site; site._id = src["_id"].get!string; site.position = deserializeJson!Point(src["position"]); return site; }}When toJson/fromJson exist, vibe.d uses them instead of the default field-by-field serialization.
Relation Serialization
Section titled “Relation Serialization”Model references are serialized differently depending on the direction:
In Responses (Full Objects)
Section titled “In Responses (Full Objects)”Relations are serialized as full objects in responses:
{ "campaign": { "_id": "abc", "title": "Save the Park", "team": { "_id": "xyz", "name": "Green Team" } }}In Requests (ID Strings)
Section titled “In Requests (ID Strings)”Clients send relations as ID strings:
{ "campaign": { "title": "Save the Park", "team": "xyz" }}The itemResolver function uses crateGetters to resolve the ID to a full object:
Json itemResolver(string modelName, string id) { if (auto getter = modelName in crateGetters) { return (*getter)(id); } throw new Exception("No getter for " ~ modelName ~ " model");}This happens in CreateItemApiOperation and UpdateItemApiOperation using the Lazy!T wrapper, which defers relation resolution until the object is actually needed.
Response Mappers
Section titled “Response Mappers”Before serialization, response items pass through any registered @mapper middleware:
@mapperJson mapper(HTTPServerRequest req, const Json item) @trusted nothrow { Json result = item.clone; // Add computed fields result["displayName"] = result["firstName"].get!string ~ " " ~ result["lastName"].get!string; // Remove sensitive fields result.remove("passwordHash"); return result;}Mappers run per-item, applied by the serializer during response construction.
ETag Generation
Section titled “ETag Generation”Operations generate ETags for caching:
- If the item has a
hashfield, use it directly - Otherwise, compute CRC32 of the JSON string representation
auto etag = item["hash"].get!string; // If available// orimport std.digest.crc : crc32Of;auto etag = crc32Of(item.toString).toHexString;The ETag is set in the response headers for conditional GET support (If-None-Match).
Streaming Responses
Section titled “Streaming Responses”List responses use InputRange!Json for memory-efficient streaming:
auto responseData = serializer.toResponseList( storage.query.exec.inputRangeObject, // Lazy stream storage);
storage.response.writeRangeBody(responseData, statusCode, mime);Items are fetched from the database, mapped, and written to the response stream one at a time. This prevents loading entire collections into memory.