There are often types that are fundamentally opaque wrappers around a scalar type. One such example is atomic.Bool and friends. In order to get these types to operate well with serialization libraries, they would need to implement one of the Marshaler or Unmarshaler interfaces (see #54582).
The current interfaces in the "encoding" package fall short:
encoding.TextMarshaler and encoding.TextUnmarshaler only treat the type as if it were a string, and so can't natively represent a boolean or number.
json.Marshaler and json.Unmarshaler is specific to JSON, which means that only "encoding/json" benefits and not other encoding libraries (in stdlib such as "encoding/xml" or third-party packages).
- The
MarshalText or MarshalJSON always allocates, which is rather heavy-weight for something that is encoding a scalar.
To work around these detriments, I propose the addition of the following interface into the "encoding" package:
type ScalarMarshaler[T bool | int64 | uint64 | float64 | complex128] interface {
MarshalScalar() (T, error)
}
type ScalarUnmarshaler[T bool | int64 | uint64 | float64 | complex128] interface {
UnmarshalScalar(T) error
}
The type list specifies exactly 5 types, which matches the superset kind of all the scalar kinds in Go:
- int8, int16, int32, int, etc. are not included since they can be represented as int64. When unmarshaling, the implementation can return an error if the provided int64 is out of range.
- We do not declare the constraint as using the underlying kind (i.e., not
~bool | ~int64 | ~uint64 | ~float64 | ~complex128) to keep the possible set of concrete interfaces a finite set that encoding packages can actually check for.
- string is not included because 1) a string is technically not a "scalar" type in that it can represent an infinite number of values, while scalar pertains to a finite set of possible values (i.e., following some scale), and 2) because we already provide
TextMarshaler and TextUnmarshaler, which are more or less the same thing.
Many serialization formats have first-class support booleans and various numeric types, so this interface provides a way to express that fact.
It is a feature (not a bug) that the 5 possible representations of this interface all use the same method name. That is, for a given type, it can only choose to implement scalar representation for one of the scalar kinds. We do not need to worry what happens when a type implements both MarshalScalar() (int64, error) and MarshalScalar() (float64, error) at the same time.
If MarshalJSON and MarshalText and MarshalScalar are all present, it is the discretion of the encoding package to choose the precedence order. I recommend MarshalJSON taking precedence over MarshalText, taking precedence over MarshalScalar.
It is up to an encoding package to decide which of the scalar kinds it wants to support. For example, "encoding/json" would support scalar marshaling for bool, int64, uint64, and float64, but reject complex128 since JSON has no representation of complex numbers. This matches the existing "encoding/json" behavior where it can't serialize complex128.
\cc @johanbrandhorst @mvdan
There are often types that are fundamentally opaque wrappers around a scalar type. One such example is
atomic.Booland friends. In order to get these types to operate well with serialization libraries, they would need to implement one of theMarshalerorUnmarshalerinterfaces (see #54582).The current interfaces in the "encoding" package fall short:
encoding.TextMarshalerandencoding.TextUnmarshaleronly treat the type as if it were a string, and so can't natively represent a boolean or number.json.Marshalerandjson.Unmarshaleris specific to JSON, which means that only "encoding/json" benefits and not other encoding libraries (in stdlib such as "encoding/xml" or third-party packages).MarshalTextorMarshalJSONalways allocates, which is rather heavy-weight for something that is encoding a scalar.To work around these detriments, I propose the addition of the following interface into the "encoding" package:
The type list specifies exactly 5 types, which matches the superset kind of all the scalar kinds in Go:
~bool | ~int64 | ~uint64 | ~float64 | ~complex128) to keep the possible set of concrete interfaces a finite set that encoding packages can actually check for.TextMarshalerandTextUnmarshaler, which are more or less the same thing.Many serialization formats have first-class support booleans and various numeric types, so this interface provides a way to express that fact.
It is a feature (not a bug) that the 5 possible representations of this interface all use the same method name. That is, for a given type, it can only choose to implement scalar representation for one of the scalar kinds. We do not need to worry what happens when a type implements both
MarshalScalar() (int64, error)andMarshalScalar() (float64, error)at the same time.If
MarshalJSONandMarshalTextandMarshalScalarare all present, it is the discretion of the encoding package to choose the precedence order. I recommendMarshalJSONtaking precedence overMarshalText, taking precedence overMarshalScalar.It is up to an encoding package to decide which of the scalar kinds it wants to support. For example, "encoding/json" would support scalar marshaling for bool, int64, uint64, and float64, but reject complex128 since JSON has no representation of complex numbers. This matches the existing "encoding/json" behavior where it can't serialize complex128.
\cc @johanbrandhorst @mvdan