Proposal Details
This is a sub-issue of the "encoding/json/v2" proposal (#71497).
Here we propose additional API to support user-defined format flags and option values. This builds on top of the v2 API and does not block the acceptance of v2.
package json // encoding/json/v2
// WithFormat constructs an option specifying the format for type T,
// which must be a concrete (i.e., non-interface) type.
// The format alters the default representation of certain types
// (see [Marshal] and [Unmarshal] for which format flags are supported).
// Later occurrences of a format option for a particular type override
// prior occurrences of a format option of the exact same type.
//
// For example, to specify that [time.Time] types format as
// a JSON number of seconds since the Unix epoch:
//
// opts := json.WithFormat[time.Time]("unix")
//
// The format can be retrieved using:
//
// v, ok := json.GetOption(opts, json.WithFormat[MyType])
//
// The format option is automatically provided when a format flag
// is specified on a Go struct field, but is only present for
// the current JSON nesting depth.
//
// For example, suppose we marshal a value of this type:
//
// type MyStruct struct {
// MyField MyType `json:",format:CustomFormat"`
// }
//
// and the "json" package calls a custom [MarshalerTo] method:
//
// func (MyType) MarshalJSONTo(enc *jsontext.Encoder, opts json.Options) error {
// // Check whether any format is specified.
// // Assuming this is called within the context of MyStruct.MyField,
// // this reports "CustomFormat".
// ... := json.GetOption(opts, json.WithFormat[MyType])
//
// // Begin encoding of a JSON object.
// ... := enc.WriteToken(jsontext.ObjectStart)
//
// // Checking the format after starting a JSON object does not
// // report "CustomFormat" since the nesting depth has changed.
// // It may still report a format if WithFormat[MyType](...)
// // was provided to the top-level marshal call.
// ... := json.GetOption(opts, json.WithFormat[MyType])
//
// // End encoding of a JSON object.
// ... := enc.WriteToken(jsontext.ObjectStart)
//
// // Checking the format reports "CustomFormat" again
// // since the encoder is back at the original depth.
// ... := json.GetOption(opts, json.WithFormat[MyType])
//
// ...
// }
//
// The format flag on a Go struct field takes precedence
// over any caller-specified format options.
//
// [WithFormat] and [WithOption] both support user-defined options,
// but the former can only represent options as a Go string,
// while the latter can represent arbitrary structured Go values.
func WithFormat[T any](v string) Options
// WithOption constructs a user-defined option value.
// The type T must be a declared, concrete (i.e., non-interface) type
// in a package or a pointer to such a type.
// Later occurrences of an option for a particular type override
// prior occurrences of an option of the exact same type.
//
// A user-defined option can be constructed using:
//
// var v MyOptionsType = ...
// opts := json.WithOption(v)
//
// The option value can be retrieved using:
//
// v, ok := json.GetOption(opts, json.WithOption[MyOptionsType])
//
// User-defined options do not affect the default JSON representation
// of any type and is only intended to alter the representation for
// user-defined types with custom JSON representation.
//
// [WithOption] and [WithFormat] both support user-defined options,
// but the former can represent arbitrary structured Go values,
// while the latter can only represent options as a Go string.
func WithOption[T any](v T) Options
Example third-party package that supports custom formats:
package geo
// Coordinate represents a position on the earth.
type Coordinate struct { ... }
func (c Coordinate) MarshalJSONTo(enc *jsontext.Encoder, opts json.Options) error {
format, _ := json.GetOption(opts, json.WithFormat[Coordinate])
switch format {
case DecimalDegrees: ...
case PlusCodes: ...
case ...
}
}
const (
// DecimalDegrees formats a coordinate as decimal degrees.
// E.g., "40.7128, -74.0060"
DecimalDegrees = "DecimalDegrees"
// PlusCodes formats a coordinate as a plus code.
// E.g., "87C8P3MM+XX"
PlusCodes = "PlusCodes"
...
)
Example usage of custom formats supported by the geo
package:
// Marshal a map of coordinates where each coordinate uses PlusCodes.
var locations map[string]geo.Coordinate = ...
json.Marshal(locations, json.WithFormat[geo.Coordinate](geo.PlusCodes))
// Marshal a Go struct with a field of a Coordinate type
// such that the field uses DecimalDegrees.
var person struct {
Name string
Location geo.Coordinate `json:",format:DecimalDegrees"`
}
json.Marshal(person)
Example third-party package that supports custom options:
package protojson
// MarshalOptions contains options to alter marshaling behavior
// specific to protobuf messages.
type MarshalOptions struct {
AllowPartial bool
UseProtoNames bool
UseEnumNumbers bool
...
}
// MarshalEncode encodes message m to the provided JSON encoder e.
func MarshalEncode(e *jsontext.Encoder, m proto.Message, opts json.Options) error {
protoOpts, _ := json.GetOption(opts, json.WithOption[MarshalOptions])
... // alter representation of protobuf message according to protoOpts
}
Example usage of custom options supported by the protojson
package:
var messages map[string]proto.Message
json.Marshal(messages,
json.WithMarshalers(json.MarshalToFunc(protojson.MarshalEncode)),
json.WithOption(protojson.MarshalOptions{AllowPartial: true}),
))
Both WithFormat
and WithOption
support some way for user-defined options to alter the representation of options.
For now, we do not support interface types as it is unclear whether retrieval of an option (e.g., GetOption(opts, json.WithOption[MyType])
) should also check whether any options are set for other interface types that MyType
also implements. Trying to construct an option of an interface type panics. In the future, this restriction can be lifted, but allows providing value sooner for a suspected majority of use cases.
Alternatives considered
Instead of WithOption
, we could consider making json.Option
an interface that could be implemented by any declared type (i.e., all exported methods). However, it is unclear how the "json" package would merge options together and how to retrieve the custom options type back out of a combined json.Options
. Also, this is more boilerplate for every user since they would need to implement at least one method to implement json.Option
. The proposed WithValue
API avoids unnecessary boilerplate for the user and only requires that the user declare a type, but no extra machinery.
An alternative API is to make the signature of WithValue
similar to context.WithValue
which accepts a user-provided key and value. In order to prevent conflicts between keys, the API requires that the key be a user-defined type. However, if we are going to require that, why not just make the user-defined value type the key itself? If so, we're back to a solution similar to the currently proposed WithValue
API.
Comment From: mitar
Thanks for opening this. Just for the record, while I support use cases and proposal above, I would prefer if custom marshal implementation could access full struct tags for a field currently being marshaled instead of just format (it is OK if format is exposed through additional/special flag because it is the most common case).
Comment From: dsnet
prefer if custom marshal implementation could access full struct tags for a field
I'm not sure I understand why this is needed. Most of the field tag options are specific to the serialization of the parent Go struct and the child field type should not care.
The current set of all proposed tag options are omitzero
, omitempty
, string
, case
, inline
, unknown
, and format
:
* omitzero
and omitempty
are evaluated in the context of the parent
* string
is already forward via the StringifyNumbers
option
* case
only matters with regard to name of JSON member in the context of the parent
* inline
and unknown
currently only matter in the context of the parent, but could consider allowing types with custom implementations to be inline-able
* format
is being forwarded with the proposed WithFormat
option in this issue
Comment From: mitar
The use case is that I could then do something like:
type Person struct {
Name string `json:"name"`
Surname string `json:"surname"`
SSN string `json:"ssn,omitempty" private:""`
}
json.Marshal(Person{...},
json.WithMarshalers(json.JoinMarshalers(
json.MarshalToFunc(func(enc *jsontext.Encoder, _ string, opts json.Options) error {
if _, private := opts.Tag().Lookup("private"); private {
return enc.WriteToken(jsontext.String(""))
}
return json.SkipFunc
}),
)),
)
Here I use opts.Tag()
to access reflect.StructTag
value.
(BTW, I support that options would simply be accessible through enc.Options()
.)
If in the future context could be accessible during marshaling, then I could make private fields conditioned on the current user or user's permissions.
Comment From: dsnet
I see, I was under the impression, access to struct tags was for the JSON-specific tag options, but your goal is to access user-specific information in the tags (in general). It's an interesting idea.
Technically, you could fold that information in the format
tag option, but might be an overloading of what format
was intended for.
Comment From: willfaught
I like everything but the names. WithOption is odd because it's not the only func that returns an Option. The "With" parts are odd because they're not adding an option to an existing Options. TypeFormat[T any](string) Options
and TypeValue[T any](any) Options
seem clearer, in my humble opinion.
Comment From: Merovius
(I'm so sorry, this got… extremely long. Skip to the last three lines, to get a summary of what I'd change)
I have a similar concern as @mitar, coming from a different angle. I'm playing around with json/v2
, trying to see how a generic Maybe[T any]
could marshal/unmarshal as JSON.
What trips me up is that the format
tag only applies at the current parsing depth, while WithFormat
applies recursively to the entire unmarshalling (unless explicitly overwritten).
Long dump of my thought process that shows the issue
There are two things that an "optional" JSON value could mean: 1. If it is a field, then an unset `Maybe[T]` should be omitted. Does not apply in any context but JSON objects. 2. An unset `Maybe[T]` should marshal/unmarshal as `null`. This applies in any context (so also in JSON arrays or raw JSON values). Case 1 is easily covered: We add `func (m Maybe[T]) IsZero() bool { return m.Valid }`. That way, the user can add `omitzero` to opt into semantic 1 for a particular field. Case 2 is easily covered for marshaling: We implement `MarshalerTo` with `if !m.Valid { return e.WriteToken(jsontext.Null) } return json.MarshalEncode(e, m.Value) }. For unmarshaling, we would dofunc (m *Maybe[T]) UnmarshalJSONFrom(d *jsontext.Decoder) error {
if d.PeekKind() == 'n' { // null
m.Valid, m.Value = false, *new(T)
return nil
}
return json.UnmarshalDecode(d, &m.Value)
}
There is an issue, though: `T` *itself* might use `nil` as a distinct representation. E.g. say the user has `type X []int` implement `UnmarshalerFrom` themselves and distinguishing between `null` and `[]` by setting the pointee of the receiver either to `nil`, or to `X{}`. They then want to use `type Y struct { Foo Maybe[X] "json:",omitzero" }`, to make having that field optional, but *if* it is present, still distinguish `null` from `[]`. That wouldn't be possible with what we have so far. It's a rare case, but it would be nice to support.
The `WithOption` API is exactly what I would have come up with. So we could add:
type omitOnly struct{}
// OmitOnly returns a json.Option that does not treat null as an invalid value, for any Maybe[T].
func OmitOnly() json.Option {
return json.WithOption(omitOnly{})
}
func (m *Maybe[T]) UnmarshalJSONFrom(d *jsontext.Decoder) error {
_, ok := json.GetOption(d.Options(), json.WithOption[omitOnly])
if !ok && d.PeekKind() == 'n' {
m.Valid, m.Value = false, *new(T)
return nil
}
return json.UnmarshalDecode(d, &m.Value)
}
````
This applies globally, though. We can also provide an `Option` specific to a member type:
```go
type omitOnlyFor[T any] struct{}
func OmitOnlyFor[T]() json.Option {
return json.WithOption(omitOnlyFor[T])
}
func (m *Maybe[T]) UnmarshalJSONFrom(d *jsontext.Decoder) error {
_, ok1 := json.GetOption(d.Options(), json.WithOption[omitOnly])
_, ok2 := json.GetOption(d.Options(), json.WithOption[omitOnlyFor[T]])
if !(ok! || ok2) && d.PeekKind() == 'n' {
m.Valid, m.Value = false, *new(T)
return nil
}
return json.UnmarshalDecode(d, &m.Value)
}
The format part of this proposal now opens up an enticing additional knob: Add a format `omitonly`, that can be added to *specific struct fields* to trigger this behavior, making it neither global, nor global to a type:
func (m *Maybe[T]) UnmarshalJSONFrom(d *jsontext.Decoder) error {
var omitOnly bool
// code to check global options as above, OR-ing into omitOnly
if fmt, ok := json.GetOption(d.Options(), json.WithFormat[Maybe[T]]); ok && fmt == "omitonly" {
omitOnly = true
}
if omitOnly && d.PeekKind() == 'n' { … }
// etc
}
type Foo struct {
Bar Maybe[T] `json:",omitempty,format:omitonly"`
}
But, what if the user wants to use `format` *on the contained type*? They can't specify it on the `Value T` field in `Maybe[T]`. So, every problem in Programming can be solved with another layer of indirection: Let's say `Maybe[T]` allows you to delegate the format to the contained type, by adding it after `-` (I'd use `,`, but the struct tag syntax doesn't allow that):
func (m *Maybe[T]) UnmarshalJSONFrom(d *jsontext.Decoder) error {
var omitOnly bool
opts := d.Options()
if fmt, ok := json.GetOption(opts, json.WithFormat[Maybe[T]]); ok {
fmt, rest, ok := strings.Cut(fmt, "-")
switch fmt {
case "omitOnly":
omitOnly = true
case "":
default:
return errors.New("invalid format")
}
if ok {
opts = json.JoinOptions(opts, json.WithFormat[T](rest))
}
}
// etc, passing opts on to `json.UnmarshalDecode`
}
But this behaves in a fundamentally different way to the general `format` tag, because it applies *recursively to all children*, while the `format` tag only applies to the current field. That is (yeah, this is contrived):
type X struct {
Time time.Time
Child *X
}
type Y struct {
Foo Maybe[X] `json:",format:omitonly-unix"`
Bar X `json:",format:unix"`
}
In this case, something like `{"Bar":{"Time":1234,"Child":{"Time":"So, the proposed WithOption
semantics are great, huge 👍 from me. The format stuff, less so.
One immediate question I had from looking at the API is, what the type parameter to WithFormat
is for?
If we want an API to communicate "format all time.Time
values as unix
", we can already do that with
package time
type JSONFormat int
const (
JSONUnix JSONFormat = iota
// …
)
func (t time.Time) MarshalJSONTo(e *json.Encoder) error {
fmt, ok := json.GetOption(e.Options(), json.WithOption[JSONFormat])
// etc
}
package foo
func F() {
// equivalent to `json.WithFormat[time.Time]("unix")` under the proposal
json.Marshal(v, json.WithOption(time.JSONUnix))
}
That is, we don't need to associate the format with time.Time
, because the time
package can just use its own sentinel type to set the global format applicable to all fields and only package time
will look for it.
But we also don't need it to communicate the format
tag. Because the struct-tag is already specific to one field. That field is typed, we don't need an additional association. From what I can tell, the example uses GetOption(…,json.WithFormat[MyType])
only, because it uses the same API to expose "attach a format globally to the type" and "attach a format to this field" to the UnmarshalerFrom
.
So, given that we don't need the "attach a format globally to the type", we can choose a different mechanism to communicate the "attach a format to this field" information, which doesn't need a type parameter. Which would allow e.g.
// WithFormat applies fmt, as if it was attached in the struct tag to the current field.
func WithFormat(fmt string) Option
That way, my Maybe[T]
example could do
func (m *Maybe[T]) UnmarshalJSONFrom(d *jsontext.Decoder) error {
var omitOnly bool
opts := d.GetOptions()
if fmt, ok := json.GetOption(opts, json.WithFormat); ok {
fmt, rest, ok := strings.Cut(fmt, "-")
switch fmt {
case "omitonly":
omitOnly = true
case "":
default:
return errors.New("invalid format")
}
if ok {
opts = json.JoinOptions(opts, json.WithFormat(rest))
}
}
// etc
}
Now, lastly: Let's see how @mitar's comment applies to this. Say we have instead a way to communicate the tags of the current field. Say
// FieldTag returns the struct tags attached to the current field, if any.
func FieldTag(Options) (reflect.StructTag, bool)
Then my Maybe[time.Time]
example would look like this:
type X struct {
Time Maybe[time.Time] `json:",omitempty" time:"unix" maybe:"omitonly"`
}
func (m *Maybe[T]) UnmarshalJSONFrom(d *jsontext.Decoder) error {
var omitOnly bool
if tag, ok := json.FieldTag(d); ok {
if fmt, _ := tag.Lookup("maybe"); ok && fmt == "omitonly" {
omitOnly = true
}
}
// etc to get the global options
// Don't even need to worry about delegating tags, as the parsing depth does not change
return json.UnmarshalDecode(d, &m.Value)
}
That seems significantly nicer to me. It gets rid of the whole delegation logic and it completely bypasses the question of the syntax of the format tag. Which I find ugly anyways, it means struct tags now are 1. a space separated list of 2. :
-separated key-value pairs, values being 3. strings, containing 4. a comma-separated list, 5, which might be contain a :
-separated key-value pair. The syntax of struct tags is growing out of hands and it is not statically checked.
So, I believe I've come to the conclusion (after writing this extremely meandering stream-of-consciousness comment), that we should
- Get rid of
WithFormat
. Have packages liketime
provide a dedicated format-type for use withWithOption
instead. - Get rid of the
format
option in thejson
struct tag. - Add
func FieldTag
as described above, to extract the struct tags of the current field, so that packages can provide their own struct-tags to customize json formatting.
Comment From: dsnet
@Merovius, thank you for your experience report!
But this behaves in a fundamentally different way to the general format tag, because it applies recursively to all children, while the format tag only applies to the current field.
I think this is a misunderstanding that unfortunately affects much of your analysis. Today, format
is already implemented under-the-hood as only applying to the current depth, and not recursively to the sub-tree. Thus:
type S1 struct {
F1 []S2 `json:",format:emitnull"`
}
type S2 struct {
F2 []int // the format:emutnull on S1.F1 does not take effect here
}
json.Marshal(S1{F1: make([]S2, 1)})
will serialize as {"F1": [{"F2": {}}]}
instead of {"F1": [{"F2": null}]}
.
Applying to the current depth was the chosen semantic since we believed it led to less spooky action at a distance.
So, in effect the issue I'm running into is that there is no way to pass an option that says "parse this, as if this format is given as a struct tag for the current depth only".
This proposal follows the prior mentioned behavior such that custom format flags specified via a Go struct tag will also only apply at "the current depth only".
Also, keep in mind that "depth" is defined in terms of the JSON nesting depth, rather than the Go nesting depth. Thus, in your particular example of a Maybe[T]
unary composite type, the format flag will be functionally forwarded to the child T
value. While the child T
is at a lower depth in terms of the Go value tree, it is at the same JSON nesting depth since Maybe[T]
is just forwarding its JSON representation to T
in the event that the value is present.
This is similar to how the format
flag still works on a *T
since pointers are kind of like a unary composite type (i.e., a type that contains exactly one other type). A pointer doesn't affect the JSON representation so long as the pointer is non-nil:
t := time.Now()
json.Marshal(struct {
T *time.Time `json:",format:unix"`
}{&t}) // uses a Unix epoch timestamp
One downside of making the format
flag operate in terms of JSON nesting depth is that you lose the ability to distinguish whether a format
flag was intended specifically for the Maybe
type or the T
type contained within the Maybe
.
(Also, a pragmatic reason why it's defined in terms of JSON nesting depth, rather than Go value nesting depth is because we can't practically track the Go nesting depth once we call into a type-provided Marshal
method, while the JSON nesting depth is always tracked by the jsontext.Encoder
even within a Marshal
method.)
If we want an API to communicate "format all time.Time values as unix", we can already do that ... That is, we don't need to associate the format with time.Time, because the time package can just use its own sentinel type to set the global format applicable to all fields and only package time will look for it.
You are correct that every package could technically implement their own format types and sentinel format constants.
One reason I chose to make WithFormat
parameterized is because there isn't a good way to unify support for a format
flag declared on a Go struct tag and package-declared option types. Fundamentally, a Go struct tag can only represent a format
as a Go string (not even a typed one at that). This is a weakness of the Go struct tag system and something that #23637 could really help out with.
Given the 1) inability to unify the string-based format
with custom option types, and 2) my suspicion that most types that have custom formatting would still want to support Go struct tags, I concluded (perhaps wrongly) that most users will forgo custom option types and just stick with custom formatting based purely on strings (even if they lose type safety). Otherwise they would have to implement two different mechanisms for how to customize representation.
Supposing my conclusion is right, then it means that a non-parameterized WithFormat
option would be problematic because then it would apply too broadly. Thus, constraining it to a particular type ensured that we were specifying just the format for that particular type.
Say we have instead a way to communicate the tags of the current field.
func FieldTag(Options) (reflect.StructTag, bool)
I like the simplicity of the function signature, but unfortunately reflect.StructTag
is not very performant as it repeatedly does string parsing in order to lookup a tag. This still might be something we should provide, but I fear it's only a matter of time before someone complains that the performance is too poor. This also seems like a situation where #23637 would be very useful. The only reason we need to do string parsing is because a Go struct tag is fundamentally just a string!
The syntax of struct tags is growing out of hands and it is not statically checked.
Absolutely agreed; we need something like #23637! 😄
Get rid of the
format
option in thejson
struct tag.
We considered this early on in the design of json/v2. Some issues we saw were regarding scope and forwards compatibility.
Regarding scope: I can imagine the specific format chosen to actually depend on the exact serialization format. For example, in a text-based format like JSON, I would prefer a timestamp to be encoded as RFC 3339. However, for a binary format like CBOR, I would prefer a timestamp to be encoded as an integer since the Unix epoch. If we support a top-level format
tag that isn't tied to the json
package, I suspect it's only a matter of time before we want the ability to constrain it down more specifically to json
(in which case we're back to the design we have today).
Regarding forwards compatibility: If we expose a total-level format
tag, I suspect this will lead to compatibility issues over time. Imagine this scenario: json/v2
is released where it understands a top-level format
tag, however xml
, and third-party yaml
, and cbor
packages do not yet understand it (and we can't reasonably expect every encoding package to support it at the same time). Eventually, the relevant maintainers of those encoding packages add support for the top-level format
flag according to their own schedule, and then suddenly people's encoded output unexpectedly change. Had Go invented the top-level format
tag from the beginning, I think this would be more appropriate since the implementation of various encoding packages would have understood the format
tag from the beginning. In some ways, this problem is analogous to how we can't retroactively add MarshalText
and UnmarshalText
methods on various types (#10275), since doing so would break existing representation.
Comment From: mitar
About compatibility. I think this is one more argument why format
should maybe be delegated to backwards compatibility only, while json/v2 should use a different mechanism mentioned above, like returning all struct tags and having them control the formatting. With updated semantics about recursion.
About performance of reflect.StructTag
- this could probably be fixed by FieldTag
returning some other parsed-only-once struct, no? If exposing reflect.StructTag
directly is not smart. It could even be something like func FieldTag(Options, name)
or something. I would not really care.
Comment From: Merovius
@dsnet I'm a little bit confused. My understanding of the proposed semantics of WithFormat[T]
is, that it basically says "apply the given format to every value of type T
". Hence, this
type A struct {
B B
}
type B struct {
T time.Time
}
func main() {
a := A{B{time.Now()}}
fmt.Println(a, json.WithFormat[time.Time]("unix"))
}
Would print something akin to {"B":{"T": 123456}}
, despite T
being at a deeper nesting depth than the object asked to Marshal
, correct?
Because that is the recursion I am referring to. I understand that the format
tag only applies to one depth - and in fact that is my problem, that the format
tag and the WithFormat
option behave fundamentally different and there is no way to specify "pretend a format tag X was given for the current depth".
The problem is that I want to be able to use
type X struct {
A Maybe[time.Time] `json:"format:onlyomit-unix"` // exact syntax debatable
}
func main() {
var x X
fmt.Println(json.Marshal(x)) // {}, as `Maybe` will omit the unset value
x.A = Just(time.Now())
fmt.Println(json.Marshal(x)) // {"A": 123456}
}
The point is that
- There is only one struct field which can specify a format tag, but
- I need to be able to specify both a format for the
Maybe[T]
type and a format for the containedT
.
The only way I could find around that, is to in the UnmarshalJSONFrom
method on Maybe[T]
, cut off the onlyomit-
part from the tag and pass the rest on to the recursive Unmarshal(&m.Value, json.WithFormat[T]("unix"))
. But that behaves different than the tag.
That is, if I do
type X struct {
A B `format:unix`
}
type Y struct {
A Maybe[B] `format:omitonly-unix`
}
type B struct {
T time.Time
}
This would marshal X
as {"A":{"T": "2025-06-2025 19:00:05.000Z"}}
, as the format
tag only applies to the current depth. Meanwhile, Y
would marshal as {"A":{"T":12345678}}
, as it passes (after instantiation) WithFormat[time.Time]("unix")
, which applies recursively.
If WithFormat
does not apply to all nesting depths, then fair enough, I am misunderstanding something. But then I don't understand why it needs a type-parameter.
I like the simplicity of the function signature, but unfortunately reflect.StructTag is not very performant as it repeatedly does string parsing in order to lookup a tag.
Yeah I predicted that you'd say something like that. We could also do (oh, just saw that @mitar mentioned that as well):
// Behaves like FieldTag(o).Lookup(key)
func LookupFieldTag(o Option, key string) (string, bool)
As json
knows the type and field it's parsing when calling the UnmarshalJSONFrom
method, it can cache a pre-parsed representation to make the access efficient.
Regarding scope: I can imagine the specific format chosen to actually depend on the exact serialization format.
I had the same thought as well. I see the advantages of the format
tag, I just thought it would be elegant to be able to get rid of it. As long as we have a way to access struct tags, I can do what I want in any case.