Skip to content

Go: Use private interfaces instead of generating structs for discriminating types #67

@diamondburned

Description

@diamondburned

Proposal

This issue proposes that for the following JSON type definition:

{
  "Thing": {
    "discriminator": "type",
    "mapping": {
      "a": {
        "properties": {}
      },
      "b": {
        "properties": {}
      },
      "c": {
        "properties": {}
      }
    }
  }
}

jtd-codegen should generate the following Go code:

type Thing struct {
	Type string `json:"type"`

	// Value can be the following types:
	//
	//    - [A]
	//    - [B]
	//    - [C]
	//
	Value thingValue `json:"-"`
}

func (t *Thing) UnmarshalJSON(data []byte) error
func (t *Thing) MarshalJSON() ([]byte, error)

type thingValue interface {
	isThing()
}

type ThingA struct{}
type ThingB struct{}
type ThingC struct{}

func (ThingA) isThing() {}
func (ThingB) isThing() {}
func (ThingC) isThing() {}

The user would consume the API like so:

var t Thing

switch v := t.Value.(type) {
case ThingA:
	log.Println("thing contains type A")
case ThingB:
	log.Println("thing contains type B")
case ThingC:
	log.Println("thing contains type C")
default:
	log.Println("thing contains unknown type")
}

If the user makes a mistake in the type-switch, the compiler will complain:

var t Thing

switch v := t.Value.(type) {
case ThingA:
	log.Println("thing contains type A")
case string:
	// does not even compile
}

This issue is an alternative to issue #49.

Rationale

Pros:

  • Using interfaces instead of flat structs can sometimes takes up less space in
    memory if the mapping is large.
  • The user can use type-switches to determine the type of the value, which is
    more idiomatic and safer in Go than using a string field.

Cons:

  • The magic field's name Value is not ideal. This is done because we can't
    have custom marshalers and unmarshalers on an interface.
  • The magic field has an unexported type. Showing an unexported type in an
    exported type is not ideal.
  • It's not explicit which types actually satisfy TValue outside a single
    comment. This might be confusing to users.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions