Edge Cases of Updates in RESTful JSON APIs

When dealing with RESTful JSON APIs, it is common that you want to give the user the ability to CRUD resources. In such cases, the question frequently arises how to handle edge cases of update requests when dealing with read-only attributes.
22.08.2013
Andreas Profous
Tags

Introduction

When dealing with RESTful JSON APIs, it is common that you want to give the user the ability to CRUD (create, read, update, delete) resources. In such cases, the question frequently arises how to handle edge cases of update requests when dealing with read-only attributes. Let me clarify with an example. Assume we have an API for cars that users can CRUD:

    GET /cars/ferrari_f430
    =>
    {
      "id": "ferrari_f430",
      "price": "175000",
      "description": "nice car!",
      "updated_at": "2013-11-15 17:03:54 +0200"
    }

In this context,

  • id is unique and mandatory for all cars. The user may choose the ID at resource creation, but can’t update it. One may argue that it’s redundant, since the id already appears in the URL. I’ll come back to this later.
  • price and description are plain attributes, meaning they can be modified by the user. We’ll call such attributes modifiable attributes.
  • updated_at is a read-only attribute. That is, it is an attribute set by the system.

Here is how our car can be updated:

    PUT /cars/ferrari_f430, with the following body
    {
      "id: "ferrari_f430"
      "price": "175500",
      "description": "very nice car!"
    }
    =>
    HTTP 200 OK
    {
      "id": "ferrari_f430",
      "price": "175500",
      "description": "very nice car!",
      "updated_at": "2014-03-15 18:03:54 +0200"
    }

But how should the following requests be handled?

    PUT /cars/ferrari_f430, with
    {
      "price": "200000",
      "description": "expensive car!"
    }

Or:

    PUT /cars/ferrari_f430, with
    {
      "id": "ferrari_f430",
      "price": "210000",
      "description": "wow",
      "updated_at": "1995-01-01 11:00:05 +0200"
    }

Before I’ll try to come up with answers to these questions, let me first give some background.

PATCH of JSON

Why JSON and not XML, for example? See here.

What is this PATCH method? It’a new HTTP method intended for partial updates of resources. It has been specified in an RFC. However, it seems like it has not been officially approved yet. In any case, it is in widespread use already. Here is an easy-to-read introduction.

Let me note that PATCH requests in scope of these simple JSON resources are idempotent. That is, if the same PATCH request is sent multiple times, it has the same effect as if the request were just sent once. This is not guaranteed in general, since you are free to choose any PATCH payload format. For example:

    PATCH /cars/ferrari_f430, with
    {
      "add_to_price": "10000"
    }

Hence, to make this clear, the payload of PATCH requests for cars may only contain a subset of the keys of the cars resource. And the value has the semantics that it replaces the old value. Because of this convention, PATCH requests of our cars API are idempotent.

On a side note, PATCH requests can always be made idempotent by only allowing conditional PATCH requests, such that the request will fail if the resource has been updated since the client last accessed the resource, as described in the RFC itself. This approach also solves concurrency problems in case of updates from multiple users.

Partial update with PUT?

According to the HTTP specification, the body of a PUT request referring to an already existing resource “SHOULD be considered as a modified version of the one residing on the origin server”. In short: partial updates with PUT are bad style, and shouldn’t be done. That’s what PATCH is for. In other words: PUT requires all modifiable attributes to be set in the payload. Therefore:

    PUT /cars/ferrari_f430, with
    {
      "price": "200000"
    }
    =>
    HTTP 400 Bad Request
    {
      "code": 1234,
      "error": "The following attribute is not specified: description"
    }

and:

    PATCH /cars/ferrari_f430, with
    {
      "price": "200000"
    }
    =>
    HTTP 200 OK
    {
      "id": "ferrari_f430",
      "price": "200000",
      "description": "nice car!",
      "updated_at": "2014-03-15 18:03:54 +0200"
    }

Ignore read-only attributes in PUT requests

Consider an application that uses our API. Quite often, such applications will have their own models, or more generally, some internal representation of the resource of the API. For example, the application may have an object for a car. A use case for PUT in this context, is that some values have been changed in this object, and that you want to store the updated representation in the API. In this use case, the application doesn’t care if some attributes are read-only or not; they just want to update the resource and be done with it.

Therefore, in our opinion, the server should ignore read-only attributes in the payload of PUT requests:

    PUT /cars/ferrari_f430, with
    {
      "id": "ferrari_f430",
      "price": "210000",
      "description": "inflation...",
      "updated_at": "1995-01-01 11:00:05 +0200"
    }
    =>
    HTTP 200 OK
    {
      "id": "ferrari_f430",
      "price": "210000",
      "description": "inflation...",
      "updated_at": "2014-03-15 18:06:54 +0200"
    }

An interesting question is how to treat PUT requests that contain a change of the id attribute. Remember that the id is immutable, that is, users are not able to modify the ID of an existing car. Hence, we consider the id a read-only attribute when changing existing resources with PUT. So:

    PUT /cars/ferrari_f430, with
    {
      "id": "fiat_500",
      "price": "210000",
      "description": "wow",
      "updated_at": "1995-01-01 11:00:05 +0200"
    }
    =>
    HTTP 200 OK
    {
      "id": "ferrari_f430",
      "price": "210000",
      "description": "wow",
      "updated_at": "2014-03-15 18:06:54 +0200"
    }

We acknowledge that this is controversial. A mismatched ID hints to a misunderstanding in the usage of the API or to a bug; throwing a hard error might be better. It’s therefore acceptable to make an exception of the rule in this case.

Resource creation with PUT

It seems to be customary to be able to create a new resource with PUT. An analogous problem to the one depicted above also arises in this context: what to do in case the id doesn’t match the one from the URL? Being consistent with the rules above:

    PUT /cars/audi_a8, with
    {
      "id": "vw_beetle",
      "price": "20000",
      "description": "cute"
    }
    =>
    HTTP 200 OK
    {
      "id": "audi_a8",
      "price": "20000",
      "description": "cute",
      "updated_at": "2014-03-15 18:06:54 +0200"
    }

Here it becomes clear that throwing an error might indeed be better. One could justify throwing an error by saying that in scope of resource creation with PUT, the id is in fact a modifiable attribute, not a read-only one. As one can see, whether an attribute is read-only or not generally depends on the specific request, and not exclusively on the resource.

Another alternative is to actually discard the id attribute from the resource. In that case these problems simply don’t occur. However, having an id field is convenient for applications storing the representation of the resource. It’s also useful to be able to specify the id in a POST request, in order to define what ID a new resource should receive.

Yet another approach is to disallow resource creation with PUT at all. It is a rather confusing feature anyhow. On the plus side, it is an idempotent way to create resources.

Whatever choice is made, it needs to be consistent with resource update using PUT, in order to not violate the rule that PUT requests are idempotent.

Reject PATCH requests containing read-only parameters

In contrast to PUT requests, it definitely makes sense to throw an error in case it is attempted to change a read-only parameter with a PATCH request. Presumably, users expect that all attributes specified in the payload of PATCH requests are changed:

    PATCH /cars/ferrari_f430, with
    {
      "price": "210000",
      "updated_at": "1995-01-01 11:00:05 +0200"
    }
    =>
    HTTP 400 Bad Request
    {
      "code": 5431,
      "error": "The following attribute cannot be changed: updated_at"
    }

Summary

Be aware that these are just recommendations that will not be valid in all cases. There are always trade-offs with each decision. In any case, we hope these considerations are going to help you in taking decisions in the design of your APIs.