Update guidance on optional/required field serialization

This commit is contained in:
Joel Speed 2025-06-12 10:47:55 +01:00
parent 2a021721e2
commit 48f3ac9af6
No known key found for this signature in database
GPG Key ID: 6E80578D6751DEFB
1 changed files with 63 additions and 21 deletions

View File

@ -822,50 +822,92 @@ indicated in the Retry-After header, if it is present).
Fields must be either optional or required.
A field that is required means that the writer must express an opinion about the value of the field.
Required fields are always present, and readers can rely on the field being present in the object.
An optional field is one where the writer may choose to omit the field entirely.
Readers should not assume that the field is present, unless the field also has a server-side default value.
Default values can only be set on optional fields.
Optional fields have the following properties:
- They have the `+optional` comment tag in Go.
- They are a pointer type in the Go definition (e.g. `AwesomeFlag *SomeFlag`) or
have a built-in `nil` value (e.g. maps and slices).
- They are marked with the `omitempty` json struct tag in the Go definition.
- The API server should allow POSTing and PUTing a resource with this field
unset.
In most cases, optional fields should also have the `omitempty` struct tag (the
`omitempty` option specifies that the field should be omitted from the json
encoding if the field has an empty value). However, If you want to have
different logic for an optional field which is not provided vs. provided with
empty values, do not use `omitempty` (e.g. https://github.com/kubernetes/kubernetes/issues/34641).
When the field type has a built-in `nil` value, such as a map or a slice, and
your use case means that you need to be able to distinguish between
"field not set" and "field set to an empty value", you should use a pointer to
the type, even though it has a built-in `nil` value.
See https://github.com/kubernetes/kubernetes/issues/34641.
Note that for backward compatibility, any field that has the `omitempty` struct
tag will be considered to be optional, but this may change in the future and
having the `+optional` comment tag is highly recommended.
tag, and is not explicitly marked as `+required`, will be considered to be optional.
This is expected to change in the future, and new fields should explicitly set either
an `+optional` or `+required` comment tag.
Required fields have the opposite properties, namely:
Required fields have the following properties:
- They do not have an `+optional` comment tag.
- They do not have an `omitempty` struct tag.
- They are not a pointer type in the Go definition (e.g. `AnotherFlag SomeFlag`).
- They mark themselves as required explicitly with a `+required` comment tag.
- The API server should not allow POSTing or PUTing a resource with this field
unset.
- They _typically_ do not use pointer types in the Go definition (e.g. `AnotherFlag SomeFlag`), though required fields where the zero value is a valid value must use pointer types, paired with an `omitempty` struct tag to avoid spurious null serializations.
For more details on how to use pointers and `omitempty` with fields, see [Serialization of optional/required fields](#serialization-of-optionalrequired-fields).
Using the `+optional` or the `omitempty` tag causes OpenAPI documentation to
reflect that the field is optional.
### Serialization of optional/required fields
Using a pointer allows distinguishing unset from the zero value for that type.
There are some cases where, in principle, a pointer is not needed for an
optional field since the zero value is forbidden, and thus implies unset. There
are examples of this in the codebase. However:
There are some cases where, in principle, a pointer is not needed for a
field since the zero value is forbidden, and thus implies unset.
There are examples of this in the codebase. However:
- it can be difficult for implementors to anticipate all cases where an empty
value might need to be distinguished from a zero value
- structs are not omitted from encoder output even where omitempty is specified,
which is messy;
- having a pointer consistently imply optional is clearer for users of the Go
language client, and any other clients that use corresponding types
- It can be difficult for implementors to anticipate all cases where an empty
value might need to be distinguished from a zero value.
- Structs are not omitted from encoder output even where `omitempty` is specified,
which is messy.
Therefore, we ask that pointers always be used with optional fields that do not
have a built-in `nil` value.
To determine whether a field should be a pointer, consider the following:
```mermaid
graph TD;
A[Start] --> B{Is the zero value a valid user choice?};
B -- Yes --> C[Use a pointer and omitempty];
B -- No --> D{Is the field optional or required?};
D -- Optional --> E["Does the field type have a built-in nil value (map or slice)?"];
E -- Yes --> F[Do not use a pointer, use omitempty];
E -- No --> C;
D -- Required --> F
```
There are several implications of the above:
- For lists and maps where the zero valid is a valid user choice, this means that `[]` and `{}` have a semantically different meaning than unset, in this case it is appropriate to use `*[]T` or `*map[T]S` respectively.
- For `bool` types, the zero value is `false`, which is always a valid user choice. `bool` types should always be pointers, and should always use the `omitempty` tag.
- When a field is required, and the zero value is not valid, a structured client who has not expressed an explicit choice will have their request rejected by the API server based on the invalid value, rather than the field being unset.
- For example, a string with a minimum length of 1; Validation would not understand if the field was unset, or set to the empty string deliberately, but would still reject the request because it did not meet the length requirements.
- Technically, using a pointer in these cases is also acceptable, but not advised as it makes coding more complex, and increases the risk of nil pointer exceptions.
- In these cases, not using `omitempty` provides the same result, but pollutes the marshaled object with zero values and is not recommended.
- For structs, the zero value can only be valid when the struct has no required fields, and does not require at least one property to be set.
- Required structs should use `omitzero` to avoid marshalling the zero value.
#### Serialization of custom resources
When custom resources are admitted by the API server, openapi validation is applied to the object _prior_ to any structured client observing the object.
For a field where the zero value is not valid, the openapi validation will reject the object if the field is present and set to the zero value,
before a controller or validation webhook could observe the field.
This means that there is no need to distinguish in a custom resource, between unset and the zero value for fields where the zero value is not valid.
In these cases, pointers are not needed, as the zero value indicates to the structured client that the field is unset.
This can be beneficial to API authors, as it reduces the complexity of the API, and reduces the risk of nil pointer exceptions in controllers.
## Defaulting