PgSerializable
Description
Serialize json directly from postgres (9.4+).
Upgrading from version 2.x.x to 3.x.x
Serialization returns a PORO instead of a json string. If you have code like:
OJ.load(Product.where(id: ids).json)
You can replace it with:
Product.where(id: ids).json
To automatically use OJ to serialize your POROs to json strings, add this code to your initialization:
Oj.optimize_rails()
Motivation
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
Configuration
To ensure traits are valid during rails initialization instead of when accessed:
# config/initializers/pg_serializable.rb
PgSerializable.validate_traits!
Migrating from version 1 to 2
Trait validations were occasionally running before the class's traits were loaded when there are complex dependencies. These were moved out of the class definition. To maintain existing behavior, add PgSerializable.validate_traits!
to an initializer. See configuration.
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
}
]
}
]
Single Table Inheritance
It works with single table inheritance. Traits must be defined on each class individually.
Without Rails
Rails isn't a dependency of this gem. However, setting it up to work without Rails takes some work. The specs/support
folder has the initialization code needed to run the gem with just ActiveSupport
and ActiveRecord
.
License
The gem is available as open source under the terms of the MIT License.
Acknowledgements
Full credit Colin Rhodes for the idea.