Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Proposal: omitempty removal for relations #80

Open
aren55555 opened this issue Feb 14, 2017 · 3 comments

Comments

Projects
None yet
5 participants
@aren55555
Copy link
Collaborator

commented Feb 14, 2017

Proposal

This issue was filed as a proposal for the removal of the omitempty as an option in a relation tag.

Problem

To One

Say an application had the following structs, modeling a Book and the Author that wrote it:

type Book struct {
	ISBN   string  `jsonapi:"primary,book"`
	Title  string  `jsonapi:"attr,title"`
	Author *Author `jsonapi:"relation,author"`
}

type Author struct {
	ID   string `jsonapi:"primary,author"`
	Name string `jsonapi:"attr,name"`
}

Currently the application would be able to serialize the struct in two different ways:

1. Book had an Author:

  • Go:
b := &Book{
	ISBN:  "0-676-97376-0",
	Title: "Life of Pi",
	Author: &Author{
		ID:   "1",
		Name: "Yann Martel",
	},
}
  • JSON:
{
    "data": {
        "type": "book",
        "id": "0-676-97376-0",
        "attributes": {
            "title": "Life of Pi"
        },
        "relationships": {
            "author": {
                "data": {
                    "type": "author",
                    "id": "1"
                }
            }
        }
    },
    "included": [{
        "type": "author",
        "id": "1",
        "attributes": {
            "name": "Yann Martel"
        }
    }]
}

2. Book without an Author (anonymous):

  • Go:
b := &Book{
	ISBN:  "978-0615275062",
	Title: "Diary of an Oxygen Thief",
}
  • JSON:
{
  "data": {
      "type": "book",
      "id": "978-0615275062",
      "attributes": {
          "title": "Diary of an Oxygen Thief"
      },
      "relationships": {
          "author": {
              "data": null
          }
      }
  }
}

The client is to understand the first example as the Book in the payload was authored by Yann Martel. The second example is understood by the client as there was no Author for this Book. When the omitempty option is added to the Book's Author field:

type Book struct {
	ISBN   string  `jsonapi:"primary,book"`
	Title  string  `jsonapi:"attr,title"`
	Author *Author `jsonapi:"relation,author,omitempty"`
}

The representation of a Book without an Author is no longer possible. When the relation is nil the relation will be omitted from the JSON Payload:

b := &Book{
	ISBN:  "978-0615275062",
	Title: "Diary of an Oxygen Thief",
}
{
    "data": {
        "type": "book",
        "id": "978-0615275062",
        "attributes": {
            "title": "Diary of an Oxygen Thief"
        }
    }
}

With the omitempty tag present in the relation, the ability to represent an empty to-one relationships via a null resource linkage has been lost.

To Many

The same problem is present with to-many relations, lets say an application models Teams and Playerss:

type Team struct {
	ID      string    `jsonapi:"primary,team"`
	Name    string    `jsonapi:"attr,name"`
	Players []*Player `jsonapi:"relation,players"`
}

type Player struct {
	ID   string `jsonapi:"primary,player"`
	Name string `jsonapi:"attr,name"`
}

The application would be able to serialize a Team structs' Players in two ways:

1. Team had Playerss:

  • Go:
t := &Team{
	ID:   "tor",
	Name: "Toronto Maple Leafs",
	Players: []*Player{
		&Player{
			ID:   "34",
			Name: "Auston Matthews",
		},
		&Player{
			ID:   "16",
			Name: "Mitchell Marner",
		},
	},
}
  • JSON:
{
    "data": {
        "type": "team",
        "id": "tor",
        "attributes": {
            "name": "Toronto Maple Leafs"
        },
        "relationships": {
            "players": {
                "data": [{
                    "type": "player",
                    "id": "34"
                }, {
                    "type": "player",
                    "id": "16"
                }]
            }
        }
    },
    "included": [{
        "type": "player",
        "id": "34",
        "attributes": {
            "name": "Auston Matthews"
        }
    }, {
        "type": "player",
        "id": "16",
        "attributes": {
            "name": "Mitchell Marner"
        }
    }]
}

2. Team did not have any Players (ie expansion team):

  • Go:
t := &Team{
	ID:      "vgk",
	Name:    "Vegas Golden Knights",
	Players: []*Player{},
}
  • JSON:
{
    "data": {
        "type": "team",
        "id": "vgk",
        "attributes": {
            "name": "Vegas Golden Knights"
        },
        "relationships": {
            "players": {
                "data": []
            }
        }
    }
}

The client is to understand the first example as the Team in the payload had 2 Players. The second example is understood by the client as there were no Players for this Team. When the omitempty option is added to the Team's Players field:

type Team struct {
	ID      string    `jsonapi:"primary,team"`
	Name    string    `jsonapi:"attr,name"`
	Players []*Player `jsonapi:"relation,players,omitempty"`
}

The representation of a Team without Players is no longer possible. When the relation is empty ([]*Player{}) the relation will be omitted from the JSON Payload:

t := &Team{
	ID:      "vgk",
	Name:    "Vegas Golden Knights",
	Players: []*Player{},
}
{
    "data": {
        "type": "team",
        "id": "vgk",
        "attributes": {
            "name": "Vegas Golden Knights"
        }
    }
}

With the omitempty tag present in the relation, the ability to represent an empty to-many relationships via an empty array [ ] resource linkage has been lost.

Solution

From here down the code samples are relying on 1.8rc3; to install it do:

go get golang.org/x/build/version/go1.8rc3 && go1.8rc3 download

Starting in go1.8 struct tags will be ignored during type conversions (see golang/go#16085 for commentary). This means you can have something like:

type foo struct {
	ID string `test:"foo"`
}
type bar struct {
	ID string `test:"bar"`
}

f := foo{ID: "foo"}
b := bar(f)
fmt.Println(f, b)

Go 1.8rc3 output:

{foo} {foo}

Go 1.7.5 would have resulted in an error of cannot convert f (type foo) to type bar

Why does this help? This helps easily convert between different permutations of structs:

To One

Define:

type Book struct {
	ISBN   string  `jsonapi:"primary,book"`
	Title  string  `jsonapi:"attr,title"`
	Author *Author
}

type BookAndAuthor struct {
	ISBN   string  `jsonapi:"primary,book"`
	Title  string  `jsonapi:"attr,title"`
	Author *Author `jsonapi:"relation,author"`
}

Think of both of these structs as JSON presenters.

  • Book would be used when you want to send/receive a representation of a book without an Author over the wire - irrespective of whether or not the book actually even has an author.
  • BookAndAuthor would be used when you want to send/receive a representation of a book with an Author over the wire - again irrespective of whether or not that particular book had an author.

Converting between these is as easy as:

b1 := Book{
	ISBN:  "978-0615275062",
	Title: "Diary of an Oxygen Thief",
}
b2 := BookAndAuthor(b)

Depending on your server's/client's intention on what you need to send/receive over the wire you would pick the appropriate struct to use. If you only ever need to send/receive a Book with an Author you would only have the BookAndAuthor struct (you would probably also rename it to just Book at that point also). In the example above b2 would have been serialized.

Conversely, if you only ever intend send/receive a Book without the Author on the wire, you would only have the Book struct. In the example above that means you would serialize b1.

To Many

type Team struct {
	ID      string    `jsonapi:"primary,team"`
	Name    string    `jsonapi:"attr,name"`
	Players []*Player
}

type TeamAndPlayers struct {
	ID      string    `jsonapi:"primary,team"`
	Name    string    `jsonapi:"attr,name"`
	Players []*Player `jsonapi:"relation,players"`
}

Again each struct is used depending on the server's/client's intention. If you don't want Players on the wire use Team; if you do want Players on the wire use TeamAndPlayers. Again the conversion between them is trivial.

Summary

I think this solution makes more sense than the omitempty tag for these reasons:

  • Allows for the representation of empty relations; the omitempty tag obfuscates this complication
  • Even if omitempty was used in combination with go1.8, 2 structs would still be needed to cover all cases
  • Separates the logical representation of different JSON API schemas into two structs for use in distinct scenarios
  • Omitting the jsonapi struct tags is more intuitive than looking through the documentation/source to discover the omitempty functionality for a relation
@genexp

This comment has been minimized.

Copy link

commented Apr 11, 2017

Hrm. I'm confused. You might say that the point of JSON api is to be able to transmit (in an efficient way) some part of an object graph to a client. How that is done, so long as it meets spec, should be up to the user of the library. I can see why someone would want to use both of these options. For example, if I specifically requested ?include=player it might be nice to represent the player relationship with an empty array. Conversely, if I didn't specify that, I wouldn't expect the relationship to be present at all. We should offer both options.

@cbandy

This comment has been minimized.

Copy link

commented Apr 12, 2017

Relationships are part of a resource's fields.

@genexp I'm not finding a specific recommendation, but my understanding is that a server typically returns all the fields of a resource unless a sparse fieldset has been requested. The include parameter is for specifying which related resources are returned in a compound document. (Personally, perhaps) I would not expect the include parameter to affect which fields are returned.

@aren55555 I agree that omitempty should be made to play nicely with empty relationships. However, using [static] types to implement [dynamic] sparse fieldsets seems like the wrong approach. (Maybe I'm incorrect in my assumption that omitempty is designed for sparse fieldsets?)

@blainsmith

This comment has been minimized.

Copy link

commented May 25, 2017

The JSON API Spec does not say you can pick and choose which relationship you can choose to be a part of the data payload. Your API contract should define a finite set of relationship keys while optionally including query params to request included too. IF your API responds with different relationships keys for the same type then that would be a breaking API change and your clients would break.

There should never be a case when a request for the same resource type should returns different relationship keys unless you version bump your route endpoints. You can certainly return data: [] for them, but not remove them all together.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.