Skip to content

Instantly share code, notes, and snippets.

@dgeb
Last active December 26, 2015 18:49
Show Gist options
  • Save dgeb/7197550 to your computer and use it in GitHub Desktop.
Save dgeb/7197550 to your computer and use it in GitHub Desktop.
Recommendations for JSON API

Recommendations for JSON API

I'm planning a series of pull requests for JSON API, many of which are related to issues already raised on Github. I've summarized them here to emphasize common themes:

  • Center requests and responses upon primary resources and reference other resources relatively.
  • By default, make as few assumptions as possible about what the client wants.
  • Allow for a number of degrees of flexibility, all of which are optional, so that the client can customize the response.
  • Provide sufficient recommendations for common API design patterns to inform default implementations.

I touched on a lot of these topics in my talk "Building Ambitious APIs with Ruby" at the Burlington Ruby Conference this summer: video + slides

Nest related resources in a linked object

Summary

Instead of including primary and related resources together at the same level, separate the related resources in a nested object.

Background

Details

It's important to separate the primary resource from related resources because:

  • It allows the client to easily identify the primary resource among related resources of the same type. This is especially important when creating a resource without prior knowledge of its server-assigned id.
  • It clarifies issues like pagination that are specific to the primary resource.

This can be achieved by nesting related resources in a linked object. For instance:

{
  "posts": [{
    "id": "1",
    "name": "Rails is Omakase",
    "links": { 
      "author": "9",
      "related": ["2"]
    }
  }],
  "linked": {
    "posts": [{
      "id": "2",
      "name": "The Parley Letter",
      "links": { 
        "author": "9",
        "related": ["1"] 
      }
    }],
    "people": [{
      "id": "9",
      "name": "DHH"
    }]
  }
}

It's easy to imagine a scenario in which resources that are members of the primary array are secondary resources for other members of the primary array (e.g. people + friends). In those scenarios, primary resources should not be duplicated as secondary resources in linked. In other words, a single canonical representation of a resource should continue to be returned with each response.

If this change is adopted, I think we should consider returning either a singular or plural primary resource as appropriate for the request, instead of always returning an array. For instance, a request to create a resource would return just that resource as an object. I believe this is the path of least surprise and is semantically correct. Nesting related resources in linked would remove one of the primary motivations to always return an array.

Inclusion of related resources

Summary

Clarify that compound document support is optional and that assumptions should be avoided when composing a compound document. Require explicit requests for the inclusion of related resources.

Background

Details

By default, no related resources will be included when a resource is requested.

Inclusion of related resources will be supported with an include parameter. For instance:

/posts/1?include=comments

In order to include resources related to other resources, the dot-separated path of each resource should be specified:

/posts/1?include=comments.authors

Note that a request for comments.authors will not automatically also include comments in the response (although comments will obviously need to be queried in order to fulfill the request for comment authors).

Multiple related resources can be requested in a comma-separated list:

/posts/1?include=authors,comments,comments.authors

Sparse fieldsets

Summary

Provide recommendations for requesting custom per-resource fieldsets. These recommendations are optional but intended to inform default implementations.

Background

Details

There are strong client and server side performance benefits to providing a convention by which a client can request a subset of fields per resource type. Some fields are either large (like associated binary data) or expensive to calculate. In fact, any unneeded fields become expensive when multiplied over a large enough response.

Resources should return all fields by default, but may provide the option to request only specific fields with query parameters. The primary resource type's fields may be specified with the fields parameter. For example:

/people?fields=id,name,age

The fields for any resource types (including the primary resource type) can be specified by including a [resource_type]_fields parameter. For instance:

/posts?include=comments,authors&post_fields=id,title&person_fields=id,name&comment_fields=id,body

Note that post_fields and fields would be interchangeable in the previous example.

Filtering

Summary

Provide recommendations for filtering. These recommendations are optional but intended to inform default implementations.

Background

Details

Filters should apply to the primary resource.

Each filter parameter may supply one or more comma-separated values. If any value matches, then the filter condition will be considered met. For example, the following request will return posts with a status of draft OR published:

/posts?status=draft,published

Multiple filter parameters may be chained together. The following request will return posts with a status of draft AND created by the author with an id of 1:

/posts?status=draft&author=1

TBD

Perhaps include recommendations for comparison operators other than equality: gt, lt, gte, lte, etc.

Sorting

Summary

Provide recommendations for sorting. These recommendations are optional but intended to inform default implementations.

Background

Details

Sorting should apply to the primary resource.

The sort parameter should be used to specify one or more fields by which results should be sorted:

/people?sort=name,age

The default sort order should be ascending. A descending sort order can be indicated with a - prefix:

/people?sort=-name

Pagination

Summary

Provide recommendations for paginating resources. These recommendations are optional but intended to inform default implementations.

Details (TBD - Discussion Needed)

Standardize upon pagination parameters, such as page and per_page, to be used to request pages of data:

/posts?page=1&per_page=100

Although RFC5988 covers web linking and recommends using the link header field, JSON API currently recommends using the top level meta object to specify links for pagination.

So what is the best way to include rel links in meta instead of the LINK header?

A rel object could be embedded in meta. This object could contain first, last, next and prev pointers as appropriate. Of course, these links should contain any filtering and sorting parameters required to page over the current result set.

It could also be useful to allow an optional total field in meta.

{
  "meta": {
  	"total": "820",
  	"rel": {
  		"next": "/posts?author=9&page=2&per_page=100",
  		"last": "/posts?author=9&page=9&per_page=100"
  	}
  },
  "posts": [{
    "id": "1",
    "name": "Rails is Omakase",
    "links": { 
      "author": "9",
      "related": ["2"]
    },
    {
      "id": "2",
      "name": "The Parley Letter",
      "links": { 
        "author": "9",
        "related": ["1"] 
      }
    }
  },
  
  ]
}

I'd like to discuss pagination of related resources in a compound document. The top-level links object contains an href for each link. Maybe it's sufficient to say that any related resource should follow that link if the resource count exceeds the allowed limit? If that's true, what should indicate that paging is needed? It seems that rel links and total counts could be useful for related resources, and therefore, both could be duplicated for related resources.

One option would be to simply repeat the meta structure for related resources. For instance:

{
  "meta": {
  	"total": "820",
  	"rel": {
  		"next": "/posts?author=9&page=2&per_page=100",
  		"last": "/posts?author=9&page=9&per_page=100"
  	},
  	"linked": {
  		"posts.author": {
  			"total": "1"
  		},
  		"posts.comments": {
  			"total": "1050",
		  	"rel": {
		  		"next": "/comments?author=9&page=2&per_page=100",
		  		"last": "/comments?author=9&page=11&per_page=100"
		  	},
  		}
  	}  	
  },
  "posts": [
  
  ],
  "linked": {
  	"people": [
  		...
  	],
  	"comments": [
  		...
  	]
  }
}

I'm not entirely happy with how these rel links are in meta, while the spec also recommends specifying links in the sibling links object.

Maybe it would be better to include all links in links instead of meta?

One other related concern is that an array of ids (e.g. for tweets) for related resources may itself become too large to include in a response. So far, we've just discussed pagination of resources, not ids.

Needless to say, this is a non-trivial problem and needs further discussion. I'm not even sure that any recommendations should be made for pagination of related resources, but then again, it's an obvious sticking point for compound documents. Let's talk soon...

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment