Chapter 7 — Forms + serializer
6 tests covering ModelFormFor<T> parse/from_json/error aggregation/
bound-validation/null handling/insert-query emission. No DB needed.
Run with cargo test --test cookbook_chapter07_forms.
The forms core, the serializer derive, and each serializer extension (nested / method / many / validators / ViewSet wiring) all ship with passing tests:
$ cargo test --test cookbook_chapter07_forms \
--test cookbook_chapter07b_serializer \
--test cookbook_chapter07f_serializer_method_and_validators \
--test cookbook_chapter07g_nested_serializer \
--test cookbook_chapter07h_many_serializer
cookbook_chapter07_forms .................................. ok (6)
cookbook_chapter07b_serializer ............................ ok (6)
cookbook_chapter07f_serializer_method_and_validators ...... ok (4)
cookbook_chapter07g_nested_serializer ..................... ok (4)
cookbook_chapter07h_many_serializer ....................... ok (4)
Three further Chapter 7 suites need a live database, so they are not in the block
above — run them with DATABASE_URL set and --test-threads=1.
7c / 7d — the non-admin, user-facing form route
/authors/new and its edit counterpart are tenant-aware form routes outside the admin:
they boot a real server, submit real form bodies over HTTP, and assert the redirect and
the persisted row. This is the path an end user walks, as opposed to the admin's generated
form.
Verified by: tests/cookbook_chapter07c_browser_form.rs
(create), tests/cookbook_chapter07d_edit_form.rs
(open + edit an existing record).
7e validate_unique_together — friendly errors for a composite UNIQUE
What: The composite UNIQUE index from §2.18b rejects a duplicate (org_id, user_id)
pair at the database, and without a pre-check the form re-render carries the raw
duplicate key value violates unique constraint "…" string.
ModelFormFor::validate_unique_together(&pool, pk_value) walks every composite-unique
index on the model and SELECTs the conflicting tuple before the INSERT/UPDATE, turning a
hit into per-field FormErrors keyed by every column in the conflict.
When: Any form over a model carrying unique_together. This is the call §2.18b's
caveat sends you here for.
pk_value is what excludes the row being edited from its own conflict check — pass the
current PK on update, and None on create.
Verified by: tests/cookbook_chapter07e_unique_together_validator.rs
— validator_accepts_when_no_existing_pair,
validator_rejects_with_per_field_errors_on_create, validator_accepts_different_pair,
validator_excludes_own_row_on_update.
- §7.95
ModelFormFor::<T>::parse(&HashMap<String,String>)— form- encoded payload →(columns, values)with per-field bound validation. Auto PK andauto_now_addcolumns are skipped (DB fills them). →modelform_parses_form_encoded_into_typed_values - §7.95 missing required fields aggregate into
FormErrors, one entry per field. →modelform_missing_required_fields_aggregate_errors - §7.96
ModelFormFor::<T>::from_json(&serde_json::Value)— JSON request body. Null values onOption<T>write explicitSqlValue::Null. →modelform_from_json_parses_object,modelform_from_json_null_writes_explicit_null - §7.98
min/maxbounds run at parse time. Out-of-range values land inFormErrorskeyed by field. →modelform_bound_violation_lands_in_form_errors - §7.99
into_insert_query()emits anInsertQueryagainst the model's table. →modelform_into_insert_query_targets_model_table
Note:
ModelFormFor::parseskips every auto-populated field —auto_now_addtimestamps likejoined_at: Auto<DateTime<Utc>>andAuto<T>primary keys — because the database fills them on INSERT. Your form only has to carry the fields a user actually enters.
7.99b #[derive(Serializer)] — a JSON façade over a model
6 tests in tests/cookbook_chapter07b_serializer.rs exercise the
serializer derive against the cookbook's Author model. Run with
cargo test --test cookbook_chapter07b_serializer.
from_model+to_valueround-trip — every field maps from the model and lands in the JSON output. →serializer_from_model_then_to_value_round_trip#[serializer(read_only)]— excluded fromwritable_fields(), still appears in JSON output. →read_only_field_omitted_from_writable_fields#[serializer(write_only)]— excluded from JSON output, accepted on input. →write_only_field_excluded_from_json_output#[serializer(source = "x")]— renames the JSON key to a different model field. →source_attribute_renames_json_key#[serializer(skip)]— usesDefault::default(), leaves the field in JSON but excludes it fromwritable_fields(). →skip_field_uses_default_and_appears_in_json_unchangedmany_to_value— batches aVec<Model>into a JSON array. →many_to_value_batches_into_json_array
Beyond the core derive, #[derive(Serializer)] also supports:
- Nested FK —
#[serializer(nested)]on a field whose type is itself a serializer and whose model source is aForeignKey<Parent>emits the nesting glue. The FK must be loaded first (.get(&pool)or.select_related(...)). →tests/cookbook_chapter07g_nested_serializer.rs - Computed fields —
#[serializer(method = "fn_name")]calls an inherentfn(model: &T) -> FieldTypeduringfrom_model. →tests/cookbook_chapter07f_serializer_method_and_validators.rs - ViewSet ↔ Serializer wiring —
ViewSet::for_model(...).serializer::<S>()runs list/detail output through the serializer instead of the bare model. →tests/cookbook_chapter09b_viewset_serializer.rs - Collections (M2M / one-to-many) —
#[serializer(many = ChildSerializer)]emits a typedset_<field>(&mut self, &[ChildModel])setter; fetch the children (accessors are async), call the setter, then serialize. →tests/cookbook_chapter07h_many_serializer.rs - Per-field validators —
#[serializer(validate = "fn_name")]runs a per-field validator, on top of model-levelmin/max/max_length. →tests/cookbook_chapter07f_serializer_method_and_validators.rs