PgSerializable

This is experimental.

Serialize json directly from postgres (9.4+).

Why?

Models:

class Product < ApplicationRecord
  has_many :categories_products
  has_many :categories, through: :categories_products
  has_many :variations
  belongs_to :label
end
class Variation < ApplicationRecord
  belongs_to :product
  belongs_to :color
end
class Color < ApplicationRecord
  has_many :variations
end
class Label < ApplicationRecord
  has_many :products
end
class Category < ApplicationRecord
  has_many :categories_products
  has_many :products, through: :categories_products
end

Using Jbuilder+ActiveRecord:

class Api::ProductsController < ApplicationController
  def index
    @products = Product.limit(200)
                       .order(updated_at: :desc)
                       .includes(:categories, :label, variations: :color)
    render 'api/products/index.json.jbuilder'
  end
end
Completed 200 OK in 2975ms (Views: 2944.2ms | ActiveRecord: 29.9ms)

Using fast_jsonapi:

class Api::ProductsController < ApplicationController
  def index
    @products = Product.limit(200)
                       .order(updated_at: :desc)
                       .includes(:categories, :label, variations: :color)
    options = {
      include: [:categories, :variations, :label, :'variations.color']
    }

    render json: ProductSerializer.new(@products, options).serialized_json
  end
end
Completed 200 OK in 542ms (Views: 0.5ms | ActiveRecord: 29.0ms)

Using PgSerializable:

class Api::ProductsController < ApplicationController
  def index
    render json: Product.limit(200).order(updated_at: :desc).json
  end
end
Completed 200 OK in 54ms (Views: 0.1ms | ActiveRecord: 43.0ms)

Benchmarking fast_jsonapi against pg_serializable on 100 requests:

                      user     system      total        real
jbuilder        175.620000  70.750000 246.370000 (282.967300)
fast_jsonapi     37.880000   0.720000  38.600000 ( 48.234853)
pg_serializable   1.180000   0.080000   1.260000 (  4.150280)

You'll see the greatest benefits from PgSerializable for deeply nested json objects.

Installation

Add this line to your application's Gemfile:

gem 'pg_serializable'

And then execute:

$ bundle

Or install it yourself as:

$ gem install pg_serializable

Usage

In your model:

require 'pg_serializable'

class Product < ApplicationRecord
  include PgSerializable

  serializable do
    default do
      attributes :name, :id
      attribute :name, label: :test_name
    end
  end
end

You can also include it in your ApplicationRecord so all models will be serializable.

In your controller:

render json: Product.limit(200).order(updated_at: :desc).json

It works with single records:

render json: Product.find(10).json

Attributes

List attributes:

attributes :name, :id

results in:

[
  {
    "id": 503,
    "name": "Direct Viewer"
  },
  {
    "id": 502,
    "name": "Side Disc Bracket"
  }
]

Re-label individual attributes:

attributes :id
attribute :name, label: :different_name
[
  {
    "id": 503,
    "different_name": "Direct Viewer"
  },
  {
    "id": 502,
    "different_name": "Side Disc Bracket"
  }
]

Wrap attributes in custom sql

serializable do
  default do
    attributes :id
    attribute :active, label: :deleted { |v| "NOT #{v}" }
  end
end
SELECT
  COALESCE(json_agg(
    json_build_object('id', a0.id, 'deleted', NOT a0.active)
  ), '[]'::json)
FROM (
  SELECT "products".*
  FROM "products"
  ORDER BY "products"."updated_at" DESC
  LIMIT 2
) a0
[
  {
    "id": 503,
    "deleted": false
  },
  {
    "id": 502,
    "deleted": false
  }
]

Traits

serializable do
  default do
    attributes :id, :name
  end

  trait :simple do
    attributes :id
  end
end
render json: Product.limit(10).json(trait: :simple)
[
  { "id": 1 },
  { "id": 2 },
  { "id": 3 },
  { "id": 4 },
  { "id": 5 },
  { "id": 6 },
  { "id": 7 },
  { "id": 8 },
  { "id": 9 },
  { "id": 10 }
]

Associations

Supported associations:

  • belongs_to
  • has_many
  • has_many :through
  • has_and_belongs_to_many
  • has_one

belongs_to

serializable do
  default do
    attributes :id, :name
    belongs_to :label
  end
end
[
  {
    "id": 503,
    "label": {
      "name": "Piper",
      "id": 106
    }
  },
  {
    "id": 502,
    "label": {
      "name": "Sebrina",
      "id": 77
    }
  }
]

has_many

Works for nested relationships

class Product < ApplicationRecord
  serializable do
    default do
      attributes :id, :name
      has_many :variations
    end
  end
end

class Variation < ApplicationRecord
  serializable do
    default do
      attributes :id, :name
      belongs_to :color
    end
  end
end

class Color < ApplicationRecord
  serializable do
    default do
      attributes :id, :hex
    end
  end
end
[
  {
    "id": 503,
    "variations": [
      {
        "name": "Cormier",
        "id": 2272,
        "color": {
          "id": 5,
          "hex": "f4b9c8"
        }
      },
      {
        "name": "Spencer",
        "id": 2271,
        "color": {
          "id": 586,
          "hex": "2e0719"
        }
      }
    ]
  },
  {
    "id": 502,
    "variations": [
      {
        "name": "DuBuque",
        "id": 2270,
        "color": {
          "id": 593,
          "hex": "0b288f"
        }
      },
      {
        "name": "Berge",
        "id": 2269,
        "color": {
          "id": 536,
          "hex": "b2bfee"
        }
      }
    ]
  }
]

has_many :through

class Product < ApplicationRecord
  has_many :categories_products
  has_many :categories, through: :categories_products

  serializable do
    default do
      attributes :id
      has_many :categories
    end
  end
end

class Category < ApplicationRecord
  serializable do
    default do
      attributes :name, :id
    end
  end
end
[
  {
    "id": 503,
    "categories": [
      {
        "name": "Juliann",
        "id": 13
      },
      {
        "name": "Teressa",
        "id": 176
      },
      {
        "name": "Garret",
        "id": 294
      }
    ]
  },
  {
    "id": 502,
    "categories": [
      {
        "name": "Rossana",
        "id": 254
      }
    ]
  }
]

has_many_and_belongs_to_many

class Product < ApplicationRecord
  has_and_belongs_to_many :categories

  serializable do
    default do
      attributes :id
      has_and_belongs_to_many :categories
    end
  end
end

class Category < ApplicationRecord
  serializable do
    default do
      attributes :name, :id
    end
  end
end
[
  {
    "id": 503,
    "categories": [
      {
        "name": "Juliann",
        "id": 13
      },
      {
        "name": "Teressa",
        "id": 176
      },
      {
        "name": "Garret",
        "id": 294
      }
    ]
  },
  {
    "id": 502,
    "categories": [
      {
        "name": "Rossana",
        "id": 254
      }
    ]
  }
]

has_one

class Product < ApplicationRecord
  has_one :variation

  serializable do
    default do
      attributes :name, :id
      has_one :variation
    end
  end
end
[
  {
    "name": "GPS Kit",
    "id": 1003,
    "variation": {
      "name": "Gottlieb",
      "id": 4544,
      "color": {
        "id": 756,
        "hex": "67809b"
      }
    }
  },
  {
    "name": "Video Transmitter",
    "id": 1002,
    "variation": {
      "name": "Hessel",
      "id": 4535,
      "color": {
        "id": 111,
        "hex": "144f9e"
      }
    }
  }
]

Association Traits

Models:

class Product < ApplicationRecord
  has_many :variations

  serializable do
    default do
      attributes :id, :name
    end

    trait :with_variations do
      attributes :id
      has_many :variations, trait: :for_products
    end
  end
end

class Variation < ApplicationRecord
  serializable do
    default do
      attributes :id
      belongs_to: :color
    end

    trait :for_products do
      attributes :id
    end
  end
end

Controller:

render json: Product.limit(3).json(trait: :with_variations)

Response:

[
   {
      "id":1,
      "variations":[

      ]
   },
   {
      "id":2,
      "variations":[
         {
            "id":5
         },
         {
            "id":4
         },
         {
            "id":3
         },
         {
            "id":2
         },
         {
            "id":1
         }
      ]
   },
   {
      "id":3,
      "variations":[
         {
            "id":14
         },
         {
            "id":13
         },
         {
            "id":12
         },
         {
            "id":11
         },
         {
            "id":10
         },
         {
            "id":9
         },
         {
            "id":8
         },
         {
            "id":7
         },
         {
            "id":6
         }
      ]
   }
]

Development

TODO

License

The gem is available as open source under the terms of the MIT License.

Acknowledgements

Full credit Colin Rhodes for the idea.