Go interfaces and json.Unmarshal

July 16, 2025

I’m working on Go code generation from a JSON schema representing a document tree. At the root, you have a union (anyOf) of named types. Each type represents one of the possible nodes the document can have, such as heading, paragraph, and so on. Further down the tree, there are also many other types of unions. A format property could be either a string or an integer, for example.

I’ve been trying to work out how to best structure the generated Go code for well over a week now.

The representation with the least amount of unnecessary indirection is a union:

type FooUnion interface {
	isFooUnion()
}

type A string

func (v A) isFooUnion() {}

type B int

func (v B) isFooUnion() {}

type C struct {
	Hi string `json:"hi"`
}

func (v C) isFooUnion() {}

What ties the members together is a private method that makes up the union’s interface.

Where this really falls apart is with JSON parsing. Check out this Go playground. The problem is that you can’t implement a method on an interface. Even though the union members all implement the JSON unmarshal interface, Go can’t know what to do with it. Should it find all types that implement the interface and try them one after another? In which order?

The other way to represent a union is through a struct that has one nullable field for each member:

type FooUnion struct {
  A *A
  B *B
  C *C
}

Spectacularly ugly. At least it’s obvious how you’d implement the unmarshal interface. You simply add a method to FooUnion.

But what do you do, if you really want an interface for the union? For a document, it’s common to create a Node interface and implement it for each of your node types (paragraph, heading, …). How do you then parse a JSON payload that’s such a document?

input := fetchJSON() // []byte
var node ???
err := json.Unmarshal(input, &node)

Keep in mind that a document is arbitrarily nested. If you add a wrapper type just for the unmarshaling, that wrapper type will appear everywhere throughout your document tree.

Since it’s 2025 I of course asked AI but it was of no help.

You need the wrapper, as far as I can tell. Here’s another playground which also uses reflection to streamline the code.

I’ve demonstrated the worst case – a union without a discriminator. For the scenario I’ve described at the start of this entry, you’d have a type field that every node is guaranteed to have. You can first parse only that field, then decide which concrete type’s unmarshaling to follow up with.