-
Notifications
You must be signed in to change notification settings - Fork 5.3k
Update guidance on required field serialization #8486
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Update guidance on required field serialization #8486
Conversation
[APPROVALNOTIFIER] This PR is NOT APPROVED This pull-request has been approved by: JoelSpeed The full list of commands accepted by this bot can be found here.
Needs approval from an approver in each of these files:
Approvers can indicate their approval by writing |
Thinking through different types we might see, and the implications of this suggestion NullableAnything that needs to be a pointer to allow the zero value to be set, that also has StringAny required string should have a minimum length. If that minimum length is 0, or unset, then the string should be a pointer+omitempty to allow the empty string to be an explicit choice. Except when the required string is not actually a string.
Integer/FloatAs per example in the decsription, if the 0 is in the valid range of values (based on minimum/maximum markers) then pointer and omitempty should be used to allow BoolAny required bool should be a pointer+omitempty unless validation only allows the value ListsA required list should have a minimum length validation ( If the list wasn't a pointer, but had omitempty, the structured client would not be able to explicitly set an empty list. MapsThe same as lists? Not sure what the appropriate marker is for the minimum size of a map (it's ObjectsFor the zero value to be valid, an object should have no required fields, and have no validation that requires a minimum number of fields be set (e.g. If the object does have a required field, but that required field would marshal the zero value and that would be accepted by validation, then the zero value of the struct would also be considered valid. I think this would happen generally on a poorly validated set of fields. E.g. a string that doesn't have an explicit minimum length, but has not been added based on the rules outlined here. The object |
@@ -843,10 +843,10 @@ having the `+optional` comment tag is highly recommended. | |||
Required fields have the opposite properties, namely: | |||
|
|||
- 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, or implicitly by not setting an `omitempty` struct tag. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't have enough context, but could we simplify this guidance to required fields are only marked via the +required marker (given that's how it should be)
Or at least recommend it
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
+1 - explicit FTW. If declarative validation flies, I want explicit.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I also believe in being explicit, but I know some tooling is implicitly assuming this, and we have a long history of that implicitness.
If we say they should be explicitly marked, should we also add an N.B.
that explains that historically this has been implicitly detected?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If we say they should be explicitly marked, should we also add an N.B. that explains that historically this has been implicitly detected?
I was thinking something like this, yes. Recommend to be explicit and then mention in some way how some tools behave (probably these tools (thinking about controller-gen as an example) also won't change to avoid breaking changes)
/hold I've been through this with @liggitt and we need to make some more updates to the guidance. Will look at creating a decision tree to help folks understand whether or not they should be setting fields as pointers/omitempty etc |
(notes from our discussion in https://docs.google.com/document/d/1k3XxUTEjcQifSXjoy5CcQY9QttdhQ-EG-R3vrKq6SUc/edit?pli=1&resourcekey=0-5xMYchKvoisABPw9tsR3nA&tab=t.0#bookmark=id.ks1ziis4vgzz - shared with https://groups.google.com/a/kubernetes.io/g/dev, can join that to get read access) |
- 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 may use pointer types, likely paired with an `omitempty` struct tag to avoid spurious null serializations. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
"may" or "must" ?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this is must
and likely paired
, should remove likely
c2ee563
to
8856289
Compare
8856289
to
22f7099
Compare
22f7099
to
a40a583
Compare
@JoelSpeed thanks for this PR, this clarifications are really helpful! |
- 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 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
q: should this (openAPI documentation) change now that we can have omitempty on required fields?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We will need to make sure that it respects +required
tags. If the field doesn't have +required
and has omitempty
, then I think there are many tools that still assume optional and we shouldn't change that.
In the long run, we need to make sure all fields are marked as +optional
xor +required
explicitly
This is expected to change in the future and having the `+optional` comment tag is | ||
now required on new optional fields. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: maybe rephrase this to avoid interleaving optional ... required ... optional
wording (even though it refers to different contexts)
Maybe something like this:
This is expected to change in the future, and new fields should explicitly set either an
+optional
or+required
comment tag.
B -- No --> D{Is the field optional, or required?}; | ||
D -- Optional --> E[Does the field have a built-in nil value?]; | ||
E -- Yes --> F[Do not use a pointer, use omitempty]; | ||
E -- no --> C; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
E -- no --> C; | |
E -- No --> C; |
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 have a built-in nil value?]; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
D -- Optional --> E[Does the field have a built-in nil value?]; | |
D -- Optional --> E[Does the field type have a built-in nil value?]; |
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?}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
B -- No --> D{Is the field optional, or required?}; | |
B -- No --> D{Is the field optional or required?}; |
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 have a built-in nil value?]; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
would it be simpler to inline the types which do have built-in nil values? "Does the field type have a built-in nil value (map or slice)?")
- 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 marhsalled object with zero values and is not recommended. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
- In these cases, not using `omitempty` provides the same result, but pollutes the marhsalled object with zero values and is not recommended. | |
- In these cases, not using `omitempty` provides the same result, but pollutes the marshaled object with zero values and is not recommended. |
@liggitt and I were discussing serialization of required fields in a thread and realised that there are valid cases where a required field should be a pointer and should be omitempty.
Take the example:
Here, the value
0
is a valid user choice.For a structured Go client, if the field were not a pointer, and not omitempty per the existing guidance, then the structured client is not actually required to set a value for the field, as the json marshalling process will marshal
foo: 0
and set a choice for them.This defeats the purpose of the field being required for structured clients, and creates a discrepancy in behaviour between structured and unstructured clients.
Would appreciate thoughts of other API reviewers on this
CC @thockin @deads2k