Skip to content

ioki-mobility/lightweight_serializer

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

LightweightSerializer CI Status

LightweightSerializer is a gem that allows you to write serializers for your API, to define your JSON models. It is highly opinionated, and tries to use as little magic as possible, but instead requires you to explicitly write what you want to do.

As an addition, this gem also provides easy ways to generate an OpenAPI 3 compatible specification for your API endpoints.

Installation

Add this line to your application's Gemfile:

gem 'lightweight_serializer'

And then execute:

$ bundle install

Usage

Let us assume a blog data structure. Given the following serializers:

class CommentSerializer < LightweightSerializer::Serializer
  attribute :content
  attribute :name do |object|
    object.first_name + " " + object.last_name
  end
end

class UserSerializer < LightweightSerializer::Serializer
  attribute :email
  attribute :name
end

class PostSerializer < LightweightSerializer::Serializer
  attribute :title
  attribute :content

  collection :comments, serializer: CommentSerializer

  nested :author, serializer: UserSerializer
end

Serialize it with:

post = {title: 'Using LightweightSerializer', content: 'Lorem ipsum', comments: [OpenStruct.new({content: 'Great post!', first_name: 'Sarah', last_name: 'Muster'})], author: {email: 'muster@example.com', name: 'Dominik Muster'}}

serializer = PostSerializer.new(post)
serializer.as_json

This outputs a Ruby Hash:

{
  data: {
    title: "Using LightweightSerializer",
    content: "Lorem ipsum",
    comments: [
      {
        content: "Great post!",
        name: "Sarah Muster",
        type: "open_struct"
      }
    ],
    author: {
      email: "muster@example.com",
      name: "Dominik Muster",
      type: "hash"
    },
    type: "hash"
  }
}

You can pass any object or a hash to the serializer.

Serializer DSL

The following DSL can be used within a LightweightSerializer::Serializer.

attribute

Serialize an attribute (same name in the output as in the object):

attribute :name

Serialize an attribute with a different name (outputs name, but reads full_name from the object):

attribute :name, &:full_name

Note that this is the same as the following. By passing a block, it is possible to include further logic to conclude on the serialized value. The passed object in the block is the same as the one the serializer was called with.

attribute(:name) { |object| object.full_name }

Do not forget the parantheses around the attribute name when specifying a block.

This is also required for boolean methods conventionally ending with a ?:

attribute :admin, &:admin?

Conditionally include specific attributes:

class PostSerializer < LightweightSerializer::Serializer
  attribute :title # Serialized in any case
  attribute :state, condition: :admin # Serialized only if `admin` is truthy
end

PostSerializer.new(post, admin: current_user.admin?)

Move the attribute to a group:

attribute :name, group: :author

Serializer.new({name: 'Sarah'}).as_json outputs:

{
  data: {
    author: {
      name: "Sarah"
    }
  }
}

Note that it may be more readable to use group directly instead:

group :author do
  attribute :name
end

nested

Nest another object:

nested :author, serializer: AuthorSerializer

The output would be:

{
  author: {
    ...
  }
}

nested also supports the group and condition options (cf. attribute).

collection

Serializes an array of a resource:

collection :comments, serializer: CommentSerializer

The output would be:

{
  comments: [
    {
      # first comment
    },
    {
      # second comment
    }
  ]
}

collection also supports the group and condition options (cf. attribute).

When you are intending to pass multiple objects into a subserializer, you can specify a hash in the format { Class => SerializerClass } as the serializer parameter. For every object that is given to the serializer, the correct serializer class is looked up. Note, that this only checks for the exact class, you cannot use a base class as the key and expect every subclass to be matched.

If you pass in an object of a type, that is not included in the list, an ArgumentError will be raised.

If you intend to have soms sort of generic fallback serializer, that all unmatched objects should be serialized with, you can specify a :fallback option.

TechPost = Struct.new(:title, :technology)
SciencePost = Struct.new(:title, :field)
Post = Struct.new(:title)

class GenericPostSerializer < LightweightSerializer::Serializer
  attribute :title
end

class TechPostSerializer < LightweightSerializer::Serializer
  attribute :title
  attribute :technology
end

class SciencePostSerializer < LightweightSerializer::Serializer
  attribute :title
  attribute :field
end

class BlogSerializer < LightweightSerializer::Serializer
  collection :posts, serializer: {
    TechPost => TechPostSerializer,
    SciencePost => SciencePostSerial,
    fallback: GenericPostSerializer
  }
end

BlogSerializer.new({posts: [
  TechPost.new('Lorem', 'JS'),
  SciencePost.new('Ipsum', 'SE'),
  Post.new("Dolor")
]}).as_json

# {
#   data: {
#     posts: [
#       { title: "Lorem", technology: "JS", type: "tech_post" },
#       { title: "Ipsum", field: "SE", type: "science_post" },
#       { title: "Dolor" }
#     ],
#     type: "hash"
#   }
# }

no_automatic_type_field!

class PostSerializer < LightweightSerializer::Serializer
  no_automatic_type_field!
  attribute :title
end

Does not write the type attribute. The above would output:

{
  data: {
    title: '..'
  }
}

As opposed to the following if omitting no_automatic_type_field!:

{
  data: {
    title: '..',
    type: 'post'
  }
}

no_root!

class PostSerializer < LightweightSerializer::Serializer
  no_root!
  attribute :title
end

Does not write the outer data attribute. The above would output:

{
  title: '..',
  type: 'post'
}

As opposed to the following if omitting no_root!:

{
  data: {
    title: '..',
    type: 'post'
  }
}

group

Groups one or more attributes in the output nested into the given name:

group :author do
  attribute :first_name
  attribute :last_name
end

This outputs:

{
  data: {
    author: {
      first_name: "Sarah",
      last_name: "Muster"
    },
    type: "hash"
  }
}

remove_attribute

Removes an attribute from the serializer. This is useful when inheriting from a serializer, but not all attributes should be serialized.

class PersonSerializer < LightweightSerializer::Serializer
  attribute :name
  attribute :city
end

class AuthorSerializer < PersonSerializer
  remove_attribute :city
end

AuthorSerializer.new({name: 'Sarah Muster', city: 'Musterhausen'}).as_json
# {
#   data: {
#     name: "Sarah Muster",
#     type: "hash"
#   }
# }

PersonSerializer.new({name: 'Sarah Muster', city: 'Musterhausen'}).as_json
# {
#   data: {
#     name: "Sarah Muster",
#     city: "Musterhausen",
#     type: "hash"
#   }
# }

serializes

Defines the type being serialized. This is mostly useful when serializing hashes or an OpenStruct, as these would otherwise be serialized as type: "hash" and type: "open_struct".

serializes type:, model:

type should be a symbol or a string and is used exactly as given. It always has precedence over model. model can be either a Class or a string, but underscore is called on it.

Example for type:

class PostSerializer < LightweightSerializer::Serializer
  serializes type: 'Post'
  attribute :title
end

PostSerializer.new({title: 'Lorem'}).as_json
# {
#   data: {
#     title: "Lorem",
#     type: "Post"
#   }
# }

Example for model with a Class:

class Post
end

class PostSerializer < LightweightSerializer::Serializer
  serializes model: Post
  attribute :title
end

PostSerializer.new({title: 'Lorem'}).as_json
# {
#   data: {
#     title: "Lorem",
#     type: "post"
#   }
# }

Example for model with a string:

class BlogPostSerializer < LightweightSerializer::Serializer
  serializes model: 'BlogPost'
  attribute :title
end

BlogPostSerializer.new({title: 'Lorem'}).as_json
# {
#   data: {
#     title: "Lorem",
#     type: "blog_post"
#   }
# }

Options to serializers

Use skip_root to avoid data to be added:

PostSerializer.new({title: 'Lorem'}, skip_root: true).as_json

This outputs:

{title: "Lorem", type: "hash"}

This has the same effect as adding no_root! to the serializer. Note that no_root! has always precedence. You cannot readd data by specifying skip_root: false.

If the data attribute is included in the output, you can use meta to add any additional information alongside. It is most often used for pagination information, but the content of meta is arbitrary.

PostSerializer.new([{title: 'Lorem'}], meta: {page: 1, total: 10}).as_json

Outputs:

{
  data: [
    {title: "Lorem", type: "hash"}
  ],
  meta: {
    page: 1, total: 10
  }
}

Note that meta is ignored when the serializer uses no_root!.

It is possible to pass additional options to the serializer, which can then be used within it by accessing options:

class PostSerializer < LightweightSerializer::Serializer
  allow_options :current_user

  attribute :author do
    options[:current_user].email
  end
end

current_user = OpenStruct.new(email: "sarah@example.com")

puts PostSerializer.new({}, current_user: current_user).to_json
# => {"data":{"author":"sarah@example.com","type":"hash"}}

Documentation

Use the following to generate the OpenAPI specification for an object represented by a serializer:

LightweightSerializer::Documentation.new(PostSerializer).openapi_schema

To get any meaningful output, document the attributes in your serializers with the following additional options. Make sure that you put the documentation options after all serializer options.

additionalProperties
allOf
anyOf
default
deprecated
description
enum
example
exclusiveMaximum
exclusiveMinimum
externalDocs
format
items
maximum
maxItems
maxLength
maxProperties
minimum
minItems
minLength
minProperties
multipleOf
not
nullable
oneOf
pattern
properties
readOnly
required
title
type
uniqueItems
writeOnly
xml

Refer to the OpenAPI specification to learn about the accepted values of these options.

Rails

  • Put your serializers in app/serializers/.

  • Add an ApplicationSerializer to share common options:

    class ApplicationSerializer < LightweightSerializer::Serializer
      no_automatic_type_field!
    end

    Then inherit from it:

    class PostSerializer < ApplicationSerializer
    end
  • In a controller action, render JSON as follows:

    render json: objects, serializer: PostSerializer`

    If you want to serialize without a serializer:

    render json: objects, no_serializer: true

Development

After checking out the repo, run bin/setup to install dependencies. Then, run rake spec to run the tests. You can also run bin/console for an interactive prompt that will allow you to experiment.

To install this gem onto your local machine, run bundle exec rake install. To release a new version, update the version number in version.rb, and then run bundle exec rake release, which will create a git tag for the version, push git commits and the created tag, and push the .gem file to rubygems.org.

Contributing

Bug reports and pull requests are welcome on GitHub at https://github.com/ioki-mobility/lightweight_serializer.

License

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

About

No description, website, or topics provided.

Resources

License

Code of conduct

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published