abstract class FastJSONAPISerializer::Base(T)

Overview

Base serialization superclass.

Considering you have various models - which can be from an ORM or just simple classes

class Address
  getter id
  property street

  def initialize(@id = 101, @street = "some street")
  end
end

class PostCode
  getter id
  property code

  def initialize(@id = 101, @code = "code 24")
  end
end

class Restaurant
  property name,
    address : Nil | Address = nil,
    post_code : Nil | PostCode = nil,
    rooms : Array(Room) = [] of Room

  def initialize(@name = "big burgers")
  end

  def rating
    "Great!"
  end

  # optional static id
  def id
    1
  end

  def tables
    [Table.new(1), Table.new(2), Table.new(3)]
  end
end

class Room
  property id : Int32 = 1,
    tables : Array(Table) = [] of Table

  def initialize(@id)
  end

  def name
    "#{id}-name"
  end
end

class Table
  property number

  def initialize(@number = 1)
  end

  def room
    Room.new(number)
  end

  def id
    number
  end
end

You can define various serializers

class AddressSerializer < FastJSONAPISerializer::Base(Address)
  attributes :street
  type "address" # be specific about the JSON-API type - default to Model camelcase
end

class PostCodeSerializer < FastJSONAPISerializer::Base(PostCode)
  attributes :code
end

class RestaurantSerializer < FastJSONAPISerializer::Base(Restaurant)
  attribute :name
  attribute :rating, :Rating, if: :test_rating
  attribute :own_field

  belongs_to :address, serializer: AddressSerializer # option key-word args

  has_one :post_code, PostCodeSerializer

  has_many :rooms, RoomSerializer
  has_many :tables, TableSerializer, :Tables

  def test_rating(object, options)
    options.nil? || !options[:test]?
  end

  def own_field(_object, _options)
    12
  end

  # default meta
  def self.meta(*options)
    {:page => 0}
  end
end

class RoomSerializer < FastJSONAPISerializer::Base(Room)
  attribute :name

  has_many :tables, TableSerializer
end

class TableSerializer < FastJSONAPISerializer::Base(Table)
  attribute :number

  belongs_to :room, RoomSerializer
end

Build your serialized json

resource = Restaurant.new
resource.address = Address.new
resource.post_code = PostCode.new
room = Room.new(1)
room.tables = [Table.new(1), Table.new(2)]
resource.rooms = [room]

RestaurantSerializer.new(resource).serialize(
  except: %i(name),
  includes: {
    :address   => [:address],
    :post_code => [:post_code],
    :tables    => {:room => [:room]},
  },
  meta: {:page => 0, :limit => 50},
  options: {:test => true}
)

Example above produces next output (this one is made to be readable - real one has no newlines and indentations):

{
  "data": {
    "id": "1",
    "type": "restaurant",
    "attributes": {
      "own_field": 12
    },
    "relationships": {
      "address": {
        "data": {
          "id": "101",
          "type": "address"
        }
      },
      "post_code": {
        "data": {
          "id": "101",
          "type": "post_code"
        }
      },
      "Tables": {
        "data": [
          {
            "id": "1",
            "type": "table"
          },
          {
            "id": "2",
            "type": "table"
          },
          {
            "id": "3",
            "type": "table"
          }
        ]
      }
    }
  },
  "included": [
    {
      "id": "101",
      "type": "address",
      "attributes": {
        "street": "some street"
      }
    },
    {
      "id": "101",
      "type": "post_code",
      "attributes": {
        "code": "code 24"
      }
    },
    {
      "id": "1",
      "type": "room",
      "attributes": {
        "name": "1-name"
      },
      "relationships": {}
    },
    {
      "id": "1",
      "type": "table",
      "attributes": {
        "number": 1
      },
      "relationships": {
        "room": {
          "data": {
            "id": "1",
            "type": "room"
          }
        }
      }
    },
    {
      "id": "2",
      "type": "room",
      "attributes": {
        "name": "2-name"
      },
      "relationships": {}
    },
    {
      "id": "2",
      "type": "table",
      "attributes": {
        "number": 2
      },
      "relationships": {
        "room": {
          "data": {
            "id": "2",
            "type": "room"
          }
        }
      }
    },
    {
      "id": "3",
      "type": "room",
      "attributes": {
        "name": "3-name"
      },
      "relationships": {}
    },
    {
      "id": "3",
      "type": "table",
      "attributes": {
        "number": 3
      },
      "relationships": {
        "room": {
          "data": {
            "id": "3",
            "type": "room"
          }
        }
      }
    }
  ],
  "meta": {
    "page": 0,
    "limit": 50
  }
}

For a details about DSL specification see DSL.

Inheritance

You can DRY your serializers by inheritance - just add required attributes and/or associations in the subclasses.

class UserSerializer < Serializer::Base(User)
  attributes :name, :age
end

class FullUserSerializer < UserSerializer
  attributes :email, :created_at

  has_many :identities, IdentitySerializer
end

Included Modules

Defined in:

fast-jsonapi-serializer/base.cr

Constructors

Class Method Summary

Instance Method Summary

Constructor Detail

def self.new(resource : T | Array(T)?, _included = Set(String).new, _included_keys = Set(Tuple(String, IDAny)).new) #

Only use @resource

serializer = RestaurantSerializer.new(resource)

@_included and @_included_keys are used internally to build child serializers via associations


Class Method Detail

def self.meta(_options) #

Returns default meta options.

If this is empty and no additional meta-options are given - .meta key is avoided. To define own default meta options just override this in your serializer:

class UserSerializer < FastJSONAPISerializer::Base(User)
  def self.meta(options)
    {
      :status => "ok",
    } of Symbol => FastJSONAPISerializer::MetaAny
  end
end

Instance Method Detail

def get_type : String #

Default serializer type If class macro type(str : String) is not used the type will based on the object passed to the serializer

Format: will be underscore and downcase

class AdminUserSerializer < FastJSONAPISerializer::Base(AdminUser)
  attributes :name, :age
end
serializer = AdminUserSerializer.new(AdminUser.first)
serializer.get_type # => `admin_user`

def serialize(except : Array(Symbol) = [] of ::Symbol, includes : Array(Symbol) | Hash = [] of ::Symbol, options : Hash? = nil, meta : Hash(Symbol, MetaAny)? = nil) #

Generates a JSON formatted string.

Arguments:

  • except - array of fields which should be excluded
  • includes - definition of relation that should be included
  • options - options that will be passed to methods defined for if attribute options and .meta(options)
  • .meta - meta attributes to be added under "meta" key at root level, merged into default .meta
RestaurantSerializer.new(resource).serialize(
  except: %i(name),
  includes: {
    :address   => [:address],
    :post_code => [:post_code],
    :tables    => {:room => [:room]},
  },
  meta: {:page => 0, :limit => 50},
  options: {:test => true}
)

Includes

includes option accepts Array or Hash values. To define just a list of association of resource object - just pass an array:

RestaurantSerializer.new(object).serialize(includes: [:tables])

You can also specify deeper and more sophisticated schema by passing Hash. In this case hash values should be of Array(Symbol) | Hash | Nil type. nil is used to mark association which name is used for key as a leaf in schema tree.


def unique_key(object) #

builds de-duplication key to be used by @_included_keys(Set)