Skip to main content

Request & Response Field Contract

This page describes what api.BaseViewSet does with each field of a model: whether it appears in responses, whether clients can write it, and what happens to request keys that match no field. The generated <Model>ViewSet uses the strictest settings, which are shown in Generated viewsets.


Schema flags​

A model's Fields() schema sets two flags on each field. Together they give the field one of four roles:

EditableSerializeRoleResponsesRequests
truetrueread-write (the default)includedwritten
falsetrueread-onlyincludedignored
truefalsewrite-only, such as schema.WriteOnly() for a passwordomittedwritten
falsefalsehiddenomittedignored; rejected as unknown in strict mode

Some fields are read-only even with Editable: true, because the database owns their values: an auto-increment primary key, AutoNow, AutoNowAdd and Generated columns.

The viewset enforces these roles only through two fields that you set from the schema. The generated viewset sets both:

vs.ExcludeResponseFields = api.NonSerializableFields(&Product{}) // drop Serialize(false) fields from responses
vs.ReadOnlyRequestFields = api.NonEditableFields(&Product{}) // ignore read-only and database-owned fields in requests

Responses​

A response contains every exported struct field that has a json tag other than -, under its JSON name. A tag that gives no name, such as json:",omitempty", uses the Go field name. The viewset then removes the following keys, in order:

  1. ExcludeResponseFields.
  2. Keys not listed in the serializer's Fields(), when that list is non-empty. A nil or empty Fields() keeps every field.
  3. The serializer's Exclude() and WriteOnlyFields().

BaseViewSet has no computed response fields. Every key comes from a struct field. A value derived from other columns belongs in a Generated schema field, which the database computes and the viewset treats as read-only. Values computed in Go belong in a custom handler or a router action.

Requests​

POST, PUT and PATCH bodies must be JSON objects. For each key, the viewset does one of the following:

  • Writes it when it names a field by the name responses use (see Responses) or, for a field in the schema, by its schema name, column name, db tag or Go name. These names are matched exactly, so TITLE does not write title. A field tagged json:"-" (or without a json tag) is written only when it is write-only, and then only under its schema names. The value is converted to the field's Go type, and a value that cannot be converted fails with 400.
  • Ignores it when it names a read-only field: a key in ReadOnlyRequestFields, one of the serializer's ReadOnlyFields() (or ReadonlyFields()), or the primary key. These are matched without regard to case. The primary key in a PUT or PATCH body never changes which row is written. That row comes from the URL.
  • Ignores or rejects it when it names nothing. See Unknown keys.

The serializer's Fields() only trims responses. It does not limit which fields a request can write. Use ReadOnlyRequestFields or ReadOnlyFields() for that.

Unknown keys​

A key is known when it names a field that the viewset writes or ignores, as listed above, or when the serializer declares it in Fields(), ReadOnlyFields() or WriteOnlyFields(). A hidden field (Editable: false, Serialize: false) is not known, under any spelling.

By default, unknown keys are ignored, as in Django REST Framework. Setting RejectUnknownRequestFields rejects them instead:

vs.RejectUnknownRequestFields = true

A request with unknown keys then fails with 400 before anything else runs, and the error names each key (response abbreviated):

{
"status": 400,
"code": "VALIDATION_ERROR",
"detail": "Validation failed",
"errors": {
"titel": [{ "message": "Unknown field.", "code": "INVALID_FIELD" }]
}
}

Read-only keys that clients echo back from an earlier response, such as id and created_at, are known, so they are still ignored silently. A hidden field is reported exactly like a key that does not exist, so the response reveals nothing about it. If your serializer reads an input that has no model field, such as password_confirm, declare it in WriteOnlyFields() so that strict mode accepts it.

Null, omitted and zero​

Request valueEffect
key omittedThe field keeps its stored value. On create, it gets its schema default, or the Go zero value if there is none.
explicit zero (0, "", false)The zero value is written, except on create for the fields in Zero values the database fills on create.
null on a pointer, slice, map or interface fieldThe field is cleared to nil.
null on any other field, such as string, int64 or boolThe value is ignored, so the field keeps its current value. It is not set to zero.
null on a TypeJSON fieldJSON null is written: the bytes null for a []byte field, otherwise the zero value.

Zero values the database fills on create​

On POST, the viewset hands the model to the manager's Create, which writes the value each field holds, including false, 0 and "" on required fields and fields with a Default. A key omitted from the body first gets the field's Go-side Default. A few fields still reach Create at their zero value and are left out of the INSERT, whether the client omitted the key or sent the zero explicitly, so the database fills them (see Defaults, zero values and timestamps):

FieldZero value on createStored
Field with a DBDefault (required or not)omitted, or sent as 0, "" or falsethe database default. Use a pointer type such as *bool for a field whose zero must be storable.
Optional foreign key (ForeignKey / OneToOne column)omitted, or sent as 0 or ""NULL, not a reference to row 0. Responses show the Go zero value (0) for a non-pointer field.
Optional Unique fieldomitted, or sent as its zero valueNULL, so two blank rows do not collide on the unique constraint.
Optional field without a Defaultomitted, or sent as its zero valueNULL: the column is nullable and has no DEFAULT. Use a pointer type such as *string to store an explicit "".
UUID, JSON, decimal, date or time field, or a non-text custom DBTypesent as ""the column default or NULL, since "" is not a valid value there.
Pointer, slice, map or interface fieldomitted, or nullNULL, or the column default.
Struct value such as time.Timeomitted, or the zero timeNULL, or the column default.
AutoNow / AutoNowAdd timestamp, auto-increment primary key, generated columnalways database-ownedthe database value.

A required foreign key or required unique field is written as sent, never turned into NULL: a zero foreign key fails its foreign key constraint (400), and a second row with the same blank value fails the unique constraint (409). Updates (PUT, PATCH) write the zero value to these fields as sent.

PUT and PATCH​

PUT is a full update. Its body must supply every schema field that is:

  • required (Required: true);
  • writable by the request: Editable: true, not database-owned, not ignored as read-only above, and backed by a struct field the viewset writes (a json:"-" field counts only when it is write-only);
  • without a Go-side Default.

A missing one fails with 400 naming the field. A field with only a DBDefault is still required, because the viewset never applies a database default to a row that already exists. A write-only required field, such as a password, is never in a response, so a client must send it again on every PUT; use PATCH to change other fields without it.

Optional fields left out of a PUT keep their stored values; PUT does not reset them. PATCH is a partial update and writes only the keys present. After the keys are applied, the whole object is validated, so a PATCH that leaves a required field empty also fails with 400.


Validation order​

create, update and partial_update run their steps in the order below. A failure at any step returns 400 and stops the request. Business hooks (BeforeCreate, BeforeSave, BeforeUpdate) and persistence run only in the last step, inside the manager, so a failed request leaves stored data unchanged and runs no hooks.

  1. Parse the JSON body.
  2. Reject unknown keys, when RejectUnknownRequestFields is set.
  3. Run the serializer's Validate().
  4. Load the stored object (update only) and apply the body, converting each value to its field's type.
  5. Validate the model: Clean(), schema Clean hooks, Validate(), and schema constraints such as Required and MaxLength.
  6. Call the manager's Create or Update, which runs hooks and writes.

Generated viewsets​

forge generate --api emits a viewset for each model with this contract:

vs.ExcludeResponseFields = api.NonSerializableFields(&Product{})
vs.ReadOnlyRequestFields = api.NonEditableFields(&Product{})
vs.RejectUnknownRequestFields = true

A generated API rejects a misspelled key with 400 instead of silently dropping it. Hand-written BaseViewSet and ViewSetConfig values keep the default, which ignores unknown keys. Both have a RejectUnknownRequestFields field for opting in.