The project is in a healthy, maintained state
The JSON API specification requires associated resources included in a PATCH request, if provided, to completely replace those that already exist. ActiveRecord's #update method only does that in some circumstances. activerecord-jsonapi_update handles the rest of them.
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
 Project Readme

Update ActiveRecord instances and their associations, consistent with JSON API specification.

Why is this needed?

The JSON API specification says the following:

If a relationship is provided in the relationships member of a resource object in a PATCH request, its value ... will be replaced with the value specified in this member.

The example provided is particularly clear:

... the following PATCH request performs a complete replacement of the tags for an article:

PATCH /articles/1 HTTP/1.1
Content-Type: application/vnd.api+json
Accept: application/vnd.api+json

{
  "data": {
    "type": "articles",
    "id": "1",
    "relationships": {
      "tags": {
        "data": [
          { "type": "tags", "id": "2" },
          { "type": "tags", "id": "3" }
        ]
      }
    }
  }
}

Which, depending on your deserialization solution, would likely be interpreted as:

article_attributes = {
  id: '1',
  tag_ids: ['2', '3']
}                                                

@article.update(article_attributes) 

@article.reload.tag_ids # => [2, 3]

Which would work correctly (any tags that were previously associated with the article that were not mentioned in the new tag_ids would be destroyed).

However consider when your model accepts nested attributes, and you then want to specify a new tag as well as keep an old one:

PATCH /articles/1 HTTP/1.1
Content-Type: application/vnd.api+json
Accept: application/vnd.api+json

{
  "data": {
    "type": "articles",
    "id": "1",
    "relationships": {
      "tags": {
        "data": [
          { "type": "tags", "id": "2" },
          { "type": "tags", "id": "clientid1" }   // New tag
        ]
      }
    },
    "included": [
     {
       "type": "tag",
       "id": "clientid1",
       "attributes": {
         "name": "New Tag"
       }
     }
    ]
  }
}

This will likely get processed as (assuming you're removing client ids):

article_attributes = {
  id: '1',
  tags_attributes: {
    0: {
      id: '2'
    },
    1:  {
      name: 'New Tag'
    },
  }
} 

@article.update(article_attributes) # The new tag is given id 4  

@article.reload.tag_ids # => [2, 3, 4]

@article.update(article_attributes) correctly creates the new tag and keeps the one that should be kept (id: 2), but it doesn't get rid of the old one (id: 3).

That's when you need activerecord-jsonapi_update:

@article.jsonapi_update(article_attributes) # The new tag is given id 4  

@article.reload.tag_ids # => [2, 4]

Installation

Add this line to your application's Gemfile:

gem 'activerecord-jsonapi_update'

And then execute:

$ bundle

Or install it yourself as:

$ gem install activerecord-jsonapi_update

Usage

activerecord-jsonapi_update provides 3 new methods on ActiveRecord instances:

  • jsonapi_update instead of update
  • jsonapi_update! instead of update!
  • assign_jsonapi_attributes instead of assign_attributes

Each has the same method signature (arguments) as the ActiveRecord method it replaces.

For example:

def update
  @article = Article.find(params[:id])

  if @article.jsonapi_update(allowed_params)
    render json: @article, status: :success
  end
end

jsonapi_update does not side-step any of the normal ActiveRecord functionality (and limits itself to pre-processing the attributes it's provided), so in order to allow destroying nested attributes you must still explicitly permit it in the accepts_nested_attributes declaration.

class Article < ActiveRecord::Base
  accepts_nested_attributes_for :tags, allow_destroy: true
end

You will also have configure strong parameters correctly in your controller to permit the associated resource attributes:

def allowed_params
  params.from_jsonapi.permit(:id, tags: [:id, :name])
end

How it works

To accommodate this, nested hashes or arrays with the _attributes suffix are processed as follows:

  • For hashes that have no id value, a new record is created (matching the behaviour of the normal #update method).
  • For hashes that have an id value, a find-and-update operation will occur (again, matching #update)
  • Any existing associated records not mentioned in the *_attributes hash or array will be destroyed (which is not how the normal #update method functions.)

Should I always use jsonapi_update now?

The jsonapi_update method intends to be as light-weight as possible, but it does carry with it a slight overhead that depends on the size and complexity of the hash of values you're using to update your models.

It also only has any effect when:

  • Strong parameters permit nested parameters to update a nested resource
  • The model declares it accepts nested parameters for an associated resources that are allowed to be destroyed

So only use jsonapi_update when you actually want to support the replacement of the entire collection of associated resources on a particular endpoint and you have configured your controller and models to allow it.

Please see the relevant section of the JSON API specification for a full discussion of supporting or rejecting updating associated resources.