navree
Documentation

Serialization

What a record's JSON holds and in what order, hiding fields and appending computed ones, per-record changes, date formats, API resources with a __type, OpenAPI schemas from models, and records in queued jobs.

On this page

Records serialize to JSON with serde, so a handler returns Json(record), Json(collection) or a page as it is. A record's JSON is its model's, plus what the ORM knows about it: extra columns, appended accessors and loaded relations. The model decides what clients see: fields it hides never leave the server.

Rust
JSON

What a record's JSON holds

In this order:

  1. the model's fields, as the model's own Serialize writes them, so serde attributes keep working (rename, skip_serializing_if, serialize_with);
  2. where a cast decides a field's output, the cast's form (a formatted date, a decimal as a string);
  3. date-times in one format, RFC 3339 in UTC with microseconds (2026-10-27T08:30:00.000000Z);
  4. the extra columns the query selected, such as activities_count;
  5. the appended accessors;
  6. the loaded relations, under their names, each related record applying its own rules (null for an empty to-one relation, [] for an empty to-many one; a many-to-many record carries its pivot).

Then only the names the model makes visible are kept, when it lists any, and the hidden ones are left out. record.to_json(), to_json_pretty() and to_value() give the same JSON as text or as a serde_json::Value; a collection serializes as an array of them, and a page as {data, meta}.

Hiding fields

hidden(..) lists what never appears in a record's JSON; visible(..) lists the only things that do, for models where a field added later should stay private until someone shows it:

Rust

Names are fields or columns (matched to serde's keys), or output keys: accessors, extras, relations. #[model(hidden)] on a field adds it to the list. Hide every secret: encrypted fields hold their plain value in the record, so their JSON would too. #[serde(skip)] on a field also keeps it out, for every serializer, while hidden is the ORM's rule and can be lifted per record.

Appending computed values

appends(..) adds accessors to every record's JSON, after the fields and extras. Each name must be an accessor of the model, or the model doesn't compile.

Per-record changes

A record, or a whole collection, can change the rules before it is serialized:

Rust

set_hidden, merge_hidden, set_visible, merge_visible, set_appends and merge_appends replace or extend the lists, and the _if forms take a bool or a function of the record. hidden(), visible(), appends() and has_appended(name) read them. A collection has the same methods and applies them to every record.

record.only(["id", "email"]) and record.except(["password_hash"]) return a JSON map of some values, whatever the rules: for building an audit entry or a message, not a response.

Dates

Every date-time serializes as RFC 3339 in UTC with microseconds, whatever the database returned, so responses don't change with the database. A field with a datetime_format cast keeps its own format. To format a model's dates another way, name a function:

Rust
JSON

API resources

When the JSON a client gets differs from the model, or front-end permission checks need the subject's type, return a resource. to_resource_json() is the record's JSON plus navree-api's "__type":

JSON

A dedicated shape is a type implementing Resource<M> (an ApiResource built from a record), which a model can name as its own:

Rust

deal.resource() and deals.resources() build the model's resource, to_resource::<R>() any other, and to_resource_json_as::<R>() its JSON with __type. to_value_masked(Some(&fields)) keeps only the fields an ability permits, for field-level access. In a handler, Ok(Json(deal.resource())), or page.map(|deal| deal.resource()) for a page.

OpenAPI schemas

Record<M> implements utoipa's ToSchema, named after the model, so routes document their responses from the model itself:

Rust

The schema has one property per serialized field under its serde key, typed from the field or its cast, with hidden fields left out, the visible list respected, Option fields nullable, skip_serializing_if fields not required, and the appended accessors. #[model(schema = Type)] on a field and #[accessor(schema = Type)] give an exact schema, such as an enum's values. SimplePage<T> and CursorPage<T> have schemas too; for navree-db's Page, navree::orm::attributes::page_schema::<Record<Deal>> is a SchemaFn for routes registered by hand, and OpenAPI shows the named page type #[api] takes.

Records in queued jobs

A job carries a record's identity rather than its values, and reads the record again when it runs, so it sees the record as it is then:

Rust

A ModelRef serializes as {"model": "Deal", "key": ..}, with the names of the record's loaded relations and its connection. restore reads from the writer, without global scopes; restore_or_skip returns None for a deleted record. collection.to_refs() and ModelRefs::restore do the same for many records, in order, leaving out deleted ones.