Rustango docs
← Anleitungen

Serializer

Ein Serializer verwandelt eine Modellinstanz in eine typisierte, JSON-fertige Form — und auf dem Rückweg wieder zurück. Er ist die Schicht zwischen deinen Modellen und der JSON-Oberfläche deiner API: Deklariere ein Struct, annotiere seine Felder, und du bekommst kontrollierte Ausgabe (umbenennen, verbergen, berechnen, verschachteln), Validierung auf Feld- und Objektebene sowie eine saubere Anbindung an ViewSets.

Eine Sache solltest du dir gleich zu Beginn einprägen: ein Rustango-Serializer formt Daten, er persistiert sie nicht. Es gibt kein serializer.save(), das in die Datenbank schreibt — das erledigt das ORM. Der Serializer bildet ein Modell auf JSON ab (from_model → to_value), deklariert, welche Felder schreibbar sind, und validiert. Du kombinierst ihn mit dem ORM und den ViewSets, statt Schreibvorgänge durch ihn zu leiten.

Neu bei einem Begriff hier? — Serializer, Modell, ORM? Das Glossar erklärt jeden Begriff in klarer Sprache.

Ein Rustango-Serializer: read_only, source-Umbenennung, ein berechnetes Methodenfeld, ein verschachtelter FK und ein write_only-Feld — deklariert auf einem einzigen Struct

Quelle: rustango::serializer (ModelSerializer, #[derive(Serializer)], die #[serializer(...)]-Feldattribute) — hinter dem serializer-Feature (standardmäßig aktiv).

Lauffähige Versionen: der minimale Serializer ist Teil des getesteten getting_started_blog-Beispiels, und das vollständige Verhalten des Derive wird durch die eigenen Unit-Tests des Frameworks abgedeckt — crates/rustango/tests/serializer_derive.rs und serializer_cross_validate.rs. Wenn ein Snippet seltsam aussieht, vergleiche es damit.


Inhaltsverzeichnis


Schnellstart

Ein Serializer ist ein einfaches Struct mit #[derive(Serializer)] und einem #[serializer(model = …)], das auf das Modell zeigt, von dem es abbildet. Es benötigt zwei begleitende Derives: serde::Deserialize (damit es auch eingehendes JSON parsen kann) und Default (damit ausgeschlossene/optionale Felder initialisiert werden können).

use rustango::Serializer;
use rustango::serializer::ModelSerializer;

#[derive(Serializer, serde::Deserialize, Default)]
#[serializer(model = Post)]
pub struct PostSerializer {
    pub id: Auto<i64>,
    pub title: String,

    #[serializer(source = "body")]      // JSON key `content`, read from model.body
    pub content: String,

    #[serializer(read_only)]            // in output, never accepted on write
    pub published_at: Auto<DateTime<Utc>>,
}

Verwende ihn:

let post = Post::objects().find(42, &pool).await?.expect("post 42");

let one  = PostSerializer::from_model(&post).to_value();   // a JSON object
let many = PostSerializer::many_to_value(&posts);          // a JSON array

from_model klont die Felder des Modells in das Struct (unter Beachtung der Attribute weiter unten); to_value serialisiert es (überspringt write_only-Felder). Das ist die gesamte Kernschleife.


Der ModelSerializer-Trait

#[derive(Serializer)] implementiert ModelSerializer (plus ein serde::Serialize, das write_only respektiert, und eine OpenApiSchema-Impl unter dem openapi-Feature). Die Trait-Oberfläche:

MethodeSignaturAnmerkungen
from_modelfn(model: &Self::Model) -> SelfBildet ein Modell → Serializer ab. Generiert; nicht überschreibbar.
to_valuefn(&self) -> serde_json::ValueSerialisiert zu JSON (überspringt write_only). Überschreibbar.
manyfn(&[Self::Model]) -> Vec<Self>Batch-from_model. Überschreibbar.
many_to_valuefn(&[Self::Model]) -> serde_json::ValueBatch → JSON-Array. Überschreibbar.
writable_fieldsfn() -> &'static [&'static str]Serializer-Feldnamen, die beim Schreiben akzeptiert werden (schließt read_only, skip, method, nested, many, slug aus).
writable_source_fieldsfn() -> &'static [&'static str]Die Modellspalten der schreibbaren Felder (source-aufgelöst). Der Schreibpfad des ViewSet persistiert nur diese. Generiert.
from_writable_jsonfn(&Value) -> Result<Self, FormErrors>Baut eine Instanz aus einem Request-Body und nutzt dabei nur die schreibbaren Felder (der Rest bekommt Default-Werte); Parse-Fehler pro Feld → FormErrors. Generiert.
validatefn(&self) -> Result<(), FormErrors>Führt die deklarierten Validatoren pro Feld + feldübergreifend aus. No-op, wenn keine deklariert sind; überschreibbar.

Es gibt bewusst kein create / update / save auf dem Trait — Schreibvorgänge laufen über das ORM (model.save(&pool)). Wenn ein Serializer in ein ViewSet eingebunden ist, nutzt der Create-/Update-Pfad from_writable_json() + validate() + writable_source_fields(), um den Request zu validieren und zu filtern, bevor gespeichert wird.


Feldattribute

Alles wird über #[serializer(...)] an jedem Feld gesteuert. Der vollständige Satz:

Attributfrom_model tutIn JSON-Ausgabe?Schreibbar?
(keins)bildet vom Modell abjaja
read_onlybildet vom Modell abjanein
write_onlyDefault::default()neinja
source = "x"bildet von model.x ab (benennt um)jaja
skipDefault::default() — setze es selbstjanein
method = "fn"ruft Self::fn(&model) aufjanein
nestedfolgt einem FK → Child::from_model(parent)janein
nested(strict)dasselbe, panickt aber, wenn der FK nicht geladen wurdejanein
many = ChildSerinitialisiert Vec::new(); füllt via set_<field>(&[Child])janein
slug = "name"klont model.<source>.value()?.namejanein
validate = "fn"Validator pro Feld, ausgeführt von validate(&self)n. v.n. v.

Gegenseitig ausschließend (Compile-Fehler bei Kombination): read_only + write_only; method + source; slug + eines von method / nested / many.

Deklarative Validatoren. max_length = N, min_length = N, min = N und max = N fügen einem Feld Schreibzeit-Validierung hinzu, ohne dessen Ausgabeform zu ändern (und ein Feld ohne diese erbt die Grenzen des Modells). Siehe Validierung.

write_only ist für rein eingehende Daten (ein Passwort, ein Einmal-Token): vorhanden in writable_fields(), fehlt in der Ausgabe. skip ist das gegenteilige Schlupfloch — das Feld wird nicht aus dem Modell gelesen und ist nicht schreibbar, du befüllst es also nach from_model von Hand (z. B. eine Liste von Tag-IDs, die du separat holst).

write_only transformiert den Wert nicht. Ein write_only-Feld wird beim Schreiben akzeptiert und wortwörtlich persistiert — der Serializer hasht oder verschlüsselt es nie. Bei einem Passwort hashe es selbst (siehe Passwörter) vor save(); read_only-Felder werden umgekehrt beim Schreiben stillschweigend ignoriert statt abgelehnt.


Berechnete Felder

Mit method = "fn" wird ein Feld berechnet statt aus einer Spalte gelesen. Deklariere das Feld und schreibe dann eine zugehörige Funktion fn(&Model) -> FieldType; sie wird während from_model aufgerufen:

#[derive(Serializer, serde::Deserialize, Default)]
#[serializer(model = Post)]
pub struct PostSerializer {
    pub title: String,
    #[serializer(method = "excerpt")]
    pub excerpt: String,
}

impl PostSerializer {
    fn excerpt(model: &Post) -> String {
        model.body.chars().take(80).collect::<String>() + "…"
    }
}

Berechnete Felder sind ausgabeseitig (ausgeschlossen aus writable_fields()).


Verschachtelte Serializer

nested bettet einen weiteren Serializer ein, indem es einem geladenen Fremdschlüssel folgt. Der Typ des Feldes ist der Kind-Serializer:

#[derive(Serializer, serde::Deserialize, Default)]
#[serializer(model = Comment)]
pub struct CommentSerializer {
    pub id: Auto<i64>,
    pub body: String,
    #[serializer(nested)]               // reads the loaded `author` FK
    pub author: AuthorBrief,
}

Der FK muss bereits geladen sein (via select_related / ein Eager-Fetch). Wenn er nicht geladen wurde, fällt das Feld auf Default::default() zurück, statt zu panicken — die Produktion degradiert bei einem fehlenden Prefetch elegant. In Tests nutze #[serializer(nested(strict))], um diesen Fallback in ein Panic zu verwandeln, damit ein weggelassener Prefetch erkannt wird. Zeige mit source auf einen anders benannten FK:

#[serializer(nested, source = "owner")]
pub author: AuthorBrief,

Verschachtelte Felder sind in der Ausgabeform schreibgeschützt — schreibbare verschachtelte Objekte werden noch nicht unterstützt (siehe Grenzen).


Sammlungen (many)

Für 1:n- oder M2M-Kinder deklariert many = ChildSerializer ein Vec<…>-Feld. Da der M2M-/Related-Accessor asynchron ist, kann das Makro ihn nicht automatisch laden; es initialisiert den Vec leer und emittiert einen set_<field>(&[ChildModel])-Helfer, den du nach dem Laden der Kinder aufrufst:

#[derive(Serializer, serde::Deserialize, Default)]
#[serializer(model = Post)]
pub struct PostWithTags {
    pub id: Auto<i64>,
    pub title: String,
    #[serializer(many = TagBrief)]
    pub tags: Vec<TagBrief>,
}

// usage
let tags = post.tags_m2m().all(&pool).await?;
let mut s = PostWithTags::from_model(&post);
s.set_tags(&tags);                       // generated setter, named set_<field>
let json = s.to_value();

Slug-Related-Felder

slug = "name" gibt eine Beziehung über ein lesbares Feld aus: statt einer FK-ID oder eines vollständigen verschachtelten Objekts wird ein einzelnes benanntes Feld ausgegeben, das aus dem geladenen Elternobjekt gezogen wird.

#[derive(Serializer, serde::Deserialize, Default)]
#[serializer(model = Post)]
pub struct PostSerializer {
    pub id: Auto<i64>,
    pub title: String,
    #[serializer(slug = "name", source = "author")]   // author.name as a flat field
    pub author_name: String,
}

Wie nested liest es von einem geladenen FK und fällt auf den Default zurück, wenn nicht geladen; es dient nur der Anzeige (nicht schreibbar).


Validierung

Drei Schichten, die alle als rustango::forms::FormErrors erscheinen (und bei einem ViewSet-Schreibvorgang als 400 mit feldbasierter Fehlerkarte). Sie laufen in dieser Reihenfolge: deklarative Constraints, dann Validatoren pro Feld, dann der feldübergreifende Hook.

Deklarative Constraints (automatisch vom Modell geerbt). max_length, min_length, min und max sind Feldattribute — und wenn du sie weglässt, erbt ein Feld die max_length / min / max / choices des Modells. So wird eine #[rustango(max_length = 200)]-Spalte längengeprüft ganz ohne Serializer-Attribut. Sie werden bei jedem schreibbaren Feld geprüft und verwandeln potenzielle Datenbank-Constraint-500s in freundliche 400s:

#[serializer(model = Widget)]
struct WidgetSerializer {
    pub code: String,               // inherits the model's max_length
    #[serializer(max_length = 4)]   // overrides the model's bound
    pub note: String,
    pub priority: i64,              // inherits the model's min / max
    pub status: String,             // inherits the model's choices
}

Die Meldungen lauten: "Ensure this value has at most N characters.", "Ensure this value has at least N characters.", "Ensure this value is ≥ N." / "≤ N" und "Select a valid choice.". (min_length gibt es nur im Serializer; choices wird vom Modell geerbt — es gibt kein choices-Attribut.)

Pro Feld (benutzerdefiniert) — deklariere validate = "fn" und schreibe fn(value: &FieldType) -> Result<(), String>:

#[derive(Serializer, serde::Deserialize, Default)]
#[serializer(model = Post)]
pub struct PostSerializer {
    #[serializer(validate = "title_min_3")]
    pub title: String,
    pub body: String,
}

impl PostSerializer {
    fn title_min_3(t: &String) -> Result<(), String> {
        if t.chars().count() < 3 { Err("title must be at least 3 chars".into()) } else { Ok(()) }
    }
}

Das Derive generiert ein validate(&self), das jeden Validator pro Feld ausführt und Fehlschläge in einer nach Feldnamen indizierten FormErrors sammelt.

Feldübergreifend — deklariere einen Hook auf Struct-Ebene, und die Validatoren verschmelzen. Füge entweder #[serializer(validate = "cross_validate")] am Struct hinzu (das Result<(), FormErrors> zurückgibt), oder implementiere schlicht selbst validate(&self), wenn es keine Validatoren pro Feld gibt, die es generieren würden:

impl PostSerializer {
    pub fn validate(&self) -> Result<(), rustango::forms::FormErrors> {
        let mut errors = rustango::forms::FormErrors::default();
        if self.title.is_empty() {
            errors.add("title", "title cannot be empty");          // field error
        }
        if self.body.starts_with(&self.title) {
            errors.add_non_field("body must not repeat the title"); // object-level error
        }
        if errors.is_empty() { Ok(()) } else { Err(errors) }
    }
}

FormErrors trennt Feld-Fehler (add(field, msg), eine HashMap<String, Vec<String>>) von Nicht-Feld-Fehlern (add_non_field(msg)). Untersuche sie mit .fields(), .non_field(), .get(field), .is_empty() und kombiniere mit .merge(other). Jenseits der deklarativen Constraints oben (max_length / min_length / min / max / geerbte choices) sind benutzerdefinierte Regeln einfache Funktionen — es gibt keine email-/Regex-Magie, was die benutzerdefinierte Validierung explizit und testbar hält. Außerhalb eines ViewSet rendert das Framework FormErrors nicht automatisch in einen HTTP-Body; bilde es selbst auf deine 400-Antwort ab (die Trennung von Feld/Nicht-Feld entspricht dem Fehler-JSON eines ViewSet).


Unique-together-Validierung

Um vor dem Speichern zu prüfen, dass eine Kandidatenzeile nicht mit einem Unique-Index über mehrere Spalten kollidiert, rufe check_unique_together_pool auf:

use std::collections::HashMap;
use rustango::core::SqlValue;
use rustango::serializer::check_unique_together_pool;

let mut values: HashMap<&'static str, SqlValue> = HashMap::new();
values.insert("org_id",  SqlValue::I64(self.org_id));
values.insert("user_id", SqlValue::I64(self.user_id));

// None on insert; Some(&pk) on update so the row doesn't clash with itself.
check_unique_together_pool(&pool, Membership::SCHEMA, &values, None).await?;

Es durchläuft die deklarierten Unique-Indexe des Modells über mehrere Spalten und gibt Err(FormErrors) mit einem Nicht-Feld-Fehler pro Kollision zurück ("The fields a, b must be unique together."). Einspaltiges unique wird der Konfliktbehandlung des Inserts überlassen; partielle (unique_when)-Indexe werden übersprungen.


Hyperlink-Ausgabe

Für eine Form im Stil eines HyperlinkedModelSerializer (Ressourcen-URLs statt nackter IDs) nachbearbeiten zwei Helfer das JSON:

use rustango::serializer::{hyperlink_url, hyperlinked_to_value};
use std::collections::HashMap;

let base = PostSerializer::from_model(&post).to_value();

let mut fk_templates = HashMap::new();
fk_templates.insert("author_id", "/api/users/{pk}");

let out = hyperlinked_to_value(base, "/api/posts/{pk}", "id", &fk_templates);
// → { "url": "/api/posts/42", "author_id_url": "/api/users/7", "id": 42, ... }

hyperlink_url(template, &pk) führt eine einmalige {pk}-Ersetzung durch; hyperlinked_to_value fügt ein url auf oberster Ebene plus ein <fk>_url pro Template hinzu (Null-FK → Null-URL). Die ursprünglichen id/<fk>_id-Schlüssel bleiben erhalten (entferne sie danach, wenn du sie loswerden willst).


Listen serialisieren

many_to_value(&models) gibt ein JSON-Array serialisierter Objekte zurück. ViewSets verpacken eine Seite davon in den Standard-Umschlag:

{ "count": 100, "page": 1, "page_size": 20, "last_page": 5, "results": [ { … }, { … } ] }

(Das ist der Standard-Umschlag mit Seitennummern; siehe Pagination für die Cursor- und Limit/Offset-Formen.)


Einen Serializer mit einem ViewSet verwenden

Binde einen Serializer in ein ViewSet ein, und er steuert die gesamte REST-Ressource — Ausgabe und Eingabe, auf jedem Backend (PostgreSQL, MySQL, SQLite):

#[derive(ViewSet)]
#[viewset(model = Post, serializer = crate::PostSerializer, ordering = "-published_at")]
pub struct PostViewSet;
// or, on the builder: ViewSet::for_model(Post::SCHEMA).serializer::<PostSerializer>()…
  • Ausgabe — list / retrieve / create / update-Antworten rendern über from_model, sodass source / method / read_only / write_only das JSON formen.
  • Eingabe — create / update führen das validate() des Serializers aus (ein Fehlschlag ist ein 400 mit feldbasierter Fehlerkarte, {field: [msgs]}), und nur schreibbare Felder werden geschrieben — read_only-/berechnete Felder, die ein Client postet, werden ignoriert, source-aufgelöst auf die Modellspalte.

Das ViewSet steuert dies über drei ModelSerializer-Methoden, die das Derive generiert: validate(), writable_source_fields() und from_writable_json(). Siehe den ViewSets-Leitfaden für das vollständige Verhalten und ein durchgearbeitetes Beispiel.

Du kannst einen Serializer auch eigenständig verwenden — bilde eine Zeile ab und gib ihr JSON aus jedem beliebigen Handler aus:

let post = Post::objects().find(42, &pool).await?.expect("post 42");
let body = PostSerializer::from_model(&post).to_value();   // shaped JSON

Validierung in einem eigenen Handler

Außerhalb eines ViewSet leitet der Serializer serde::Deserialize ab, sodass du einen Request-Body in ihn parsen, .validate() ausführen und — bei Erfolg — die Daten auf ein Modell abbilden und save(&pool) kannst. from_writable_json() baut eine Instanz nur aus den schreibbaren Schlüsseln (schreibgeschützte / berechnete Felder bekommen Default-Werte), und writable_fields() / writable_source_fields() sagen dir, welche Schlüssel akzeptiert werden — dieselbe Maschinerie, die das ViewSet intern nutzt.


OpenAPI-Schemata

Mit aktivem openapi-Feature emittiert das Derive zusätzlich eine OpenApiSchema-Impl: Feldtypen bilden auf JSON-Schema-Typen ab, Option<T> wird nullable-und-nicht-erforderlich, und write_only-Felder werden aus dem Antwortschema ausgeschlossen. Das speist die generierten API-Docs — kein separates Schema zu pflegen.

Vertiefung: OpenAPI — verwandle dieses Schema (plus die CRUD-Pfade deines ViewSet) in eine vollständige OpenAPI-3.1-Spec, die mit Swagger UI / Redoc ausgeliefert wird.


Scaffolding

Generiere ein Serializer-Gerüst mit der manage-CLI:

cargo run -- make:serializer PostSerializer --model Post

Es schreibt ein Startmodul, das du ausfüllst:

//! Auto-scaffolded by `manage make:serializer PostSerializer`.

use rustango::Serializer;

#[derive(Serializer, serde::Deserialize, Default)]
#[serializer(model = Post)]
pub struct PostSerializer {
    pub id: i64,
    // pub title: String,
    // #[serializer(read_only)]
    // pub created_at: chrono::DateTime<chrono::Utc>,
}

Registriere dann das Modul (mod post_serializer;) neben deinen anderen.


Feinheiten und aktuelle Grenzen

Ein paar scharfe Kanten und Schlupflöcher, die man kennen sollte:

  • Bedingte Felder. Es gibt keine Feldauswahl zur Laufzeit (Felder sind zur Compile-Zeit fixiert). Für „nur einbeziehen, wenn vorhanden“ nutze Option<T> plus #[serde(skip_serializing_if = "Option::is_none")] am Feld — die benutzerdefinierte Serialize-Impl respektiert serde-Attribute.
  • Benutzerdefinierte Ausgabeform. Überschreibe to_value(&self) an deinem Struct für ein vollständig maßgeschneidertes JSON-Objekt, wenn die Attribute nicht ausreichen.
  • Schreibbare verschachtelte Objekte werden nicht unterstützt — nested / many / slug-Felder sind ausgabeseitig. Nimm Schreibvorgänge als skalare IDs entgegen und löse sie selbst auf.
  • Eingebaute Validatoren sind nur Länge/Bereich/Auswahl — max_length / min_length / min / max (und geerbte choices) sind deklarativ; andere Regeln (email, Regex, …) sind Funktionen, die du schreibst (siehe Validierung).
  • Ein Validator pro Feld je Feld. Für mehrere Regeln an einem Feld kombiniere sie in der Funktion dieses Feldes, oder füge ein feldübergreifendes validate(&self) hinzu.
  • Der Serializer persistiert nicht. Abbilden → validieren → die Daten an das ORM übergeben; es gibt kein serializer.save().

Probier es aus

Der minimale Serializer ist Teil des getting_started_blog-Beispiels (Schritt 13 des Getting-Started-Leitfadens). Das vollständige Verhalten des Derive — die Feldattribute, berechnete/verschachtelte/many-Felder und beide Validierungsschichten — wird durch die eigenen Unit-Tests des Frameworks abgedeckt (keine Datenbank nötig):

cd crates/rustango
cargo test --test serializer_derive          # field attrs, method, nested, many, slug, OpenAPI
cargo test --test serializer_cross_validate  # per-field + cross-field validation aggregation

Siehe auch

  • ViewSets — binde einen Serializer in eine JSON-CRUD-API ein.
  • HTML-Views — die serverseitig gerenderte Alternative zu einer JSON-API.
  • OpenAPI — die Felder eines Serializers werden zu einem Component-Schema.
  • ORM-Kochbuch — die Modelle, von denen Serializer abbilden.