This is a copy of Go's encoding/json with modifications to add "optional" and "nullable" struct field tags.
Differentiate between JSON fields that are undefined (meaning not present), null, or are Go zero-values.
Currently, Go's encoding/json
package lumps all three cases into the Go zero-value, which makes it impossible to
differentiate between them without heavy, clunky workarounds.
Use cases:
- JSON Schema, OpenAPI, etc types/code generation
- JSON Merge Patch
- anything else where zero-valued, null, and undefined/omitted fields can have different meanings
type MyStruct struct {
BasicInt int `json:""`
BasicIntPtr *int `json:""`
NullableInt *int `json:",nullable"`
OptionalInt *int `json:",optional"`
OptionalNullableInt **int `json:",optional,nullable"` // NOTE: the order of optional and nullable does not matter
}
In the above example:
BasicInt
andBasicIntPtr
are handled as vanillaencoding/json
would handle them.NullableInt
will be set tonull
if the field isnil
.NullableInt
will be set to0
if the field is&(int(0))
.OptionalInt
will be omitted from the JSON if it isnil
.OptionalInt
will be set to0
if the field is&(int(0))
.OptionalNullableInt
will be omitted from the JSON if it isnil
.OptionalNullableInt
will be set tonull
if it is&((*int)(nil))
.OptionalNullableInt
will be set to0
if the field is&(&(int(0)))
.
In the above example:
BasicInt
andBasicIntPtr
are handled as vanillaencoding/json
would handle them.- GOTCHA:
NullableInt
will return an error if the field is undefined.- This is for consistency: a
nil
NullableInt
means the field was explicitly set tonull
. - Elsewhere, I refer to this as a nullable-but-not-optional field.
- This is for consistency: a
NullableInt
will be set tonil
if the field isnull
.NullableInt
will be set to&(int(0))
if the field is0
.OptionalInt
will be set tonil
if the field is undefined.OptionalInt
will be set to&(int(0))
if the field is0
.OptionalNullableInt
will be set tonil
if the field is undefined.OptionalNullableInt
will be set to&((*int)(nil))
if the field isnull
.OptionalNullableInt
will be set to&(&(int(0)))
if the field is0
.
You can differentiate between a field that is undefined/omitted, a field that is null
, and a zero-valued field as follows:
x := MyStruct{}
json.Unmarshal(data, &x) // for example's sake, assume no error
// optional field
isUndefined := x.OptionalInt == nil
isZero := *x.OptionalInt == 0 // assuming x.OptionalInt is not nil
// nullable field
isNull := x.NullableInt == nil
isZero = *x.NullableInt == 0 // assuming x.NullableInt is not nil
// optional, nullable field
isUndefined = x.OptionalNullableInt == nil
isNull = *x.OptionalNullableInt == nil // assuming x.OptionalNullableInt is not nil
isZero = **x.OptionalNullableInt == 0 // assuming x.OptionalNullableInt and *x.OptionalNullableInt are not nil
- The
optional
andnullable
tags are not compatible with theomitempty
tag and will return an error at marshal/unmarshal time if used together. optional
andnullable
tags each require an additional level of indirection for the field.- For example, for a base type
T
:*T `json:",nullable"`
*T `json:",optional"`
**T `json:",optional,nullable"`
- this includes when
T
is a pointer type:***APtrType `json:",optional,nullable"`
- An insufficient level of indirection will return an error at marshal/unmarshal time.
- For example, for a base type
- As mentioned in the Usage > Unmarshalling section, nullable-but-not-optional fields will raise an error if the field is undefined.
- To be clear, the absence of an
optional
tag does not imply that a field is required (expect for the nullable-but-not-optional case that was just mentioned). It only means we will not perform any specialoptional
handling for the field.
- Instead of (or in addition to) using pointers for
optional
andnullable
fields, use custom types.- Perhaps
json.Nullable[T any]
,json.Optional[T any]
, andjson.OptionalNullable[T any]
types that exposeIsSet()
andIsNull()
methods. - Pro: less pointer dereferencing.
- Con: it more strongly couples struct types with this JSON package. Keeping struct definitions and JSON logic separate is a good thing. (But we're already adding pointers to accommodate JSON, so...?)
- Con: it is more verbose.
- Perhaps