Go Lexicon Struct $type

Something I've been noodling on as side-project is improving the Go lexicon codegen ("lexgen") output in indigo.

There are a couple tricky bits to this, partially because we need to support both CBOR and JSON encoding (at least for some types), and because Go's JSON tagging support is a bit minimal. That's probably the right thing for the language, but it makes supporting things like discriminated unions more complicated. The upcoming json/v2 changes look like an improvement, but don't help with these specific issues.

An example friction is the $type field on record type structs. The lexicon encoding rules are that records should always have the $type populated in the object, even if they aren't in an union. That means when stored as a record, or a sub-field of another record type, or anywhere in API requests or responses.

The straight-forward way to do this would be if Go JSON marshaling tags supported const. You could just put the NSID in there at the struct level, and it would always get inserted when serializing.

The paper-cut way is to put a Type field on the struct and tell devs they are responsible for always populating that with the NSID (maybe using a codegen const). Nobody wants to do that! Even with constructor support (NewExampleRecord()) I think it'll get skipped a bunch. But this has been the best idea I could come up with.

Maybe there is a good way to do this by declaring a MarshalJSON() method on the struct type? Eg, if you could insert a bit of code to set any constant fields, and then call the regular reflect code. But I think defining that method clobbers the reflect mechanism and I don't know how to get around that.

An idea I just came up with is to define a string wrapper that implements MarshalText() with constant output:

var ComExampleRecord_NSID = "com.example.record"

type ComExampleRecord_Type string

func (r ComExampleRecord_Type) MarshalText() ([]byte, error) {
return []byte(ComExampleRecord_NSID), nil
}

type ComExampleRecord struct {
Type ComExampleRecord_Type `json:"$type"`
Text string `json:"text"`
Year int64 `json:"year,omitempty"`
}

You can give it a try in the Go playground here: https://go.dev/play/p/9IJsJ8SFPY5