FlexiView Serializer

FlexiView is a flexible Ruby serialization library that eliminates the need for multiple serializer classes by providing dynamic views and namespaces within a single serializer definition.

Installation

Install the gem and add to the application's Gemfile by executing:

bundle add flexi_view

If bundler is not being used to manage dependencies, install the gem by executing:

gem install flexi_view

Quick Example

class ProductSerializer
  include FlexiView::Serializer

  attributes :id, :name, :price, :retailer_price
  one :offer, "OfferSerializer"

  view(:index).fields(:id, :name).association(:offer)

  namespace(:admin) do
    view(:index).fields(:id, :name, :price, :retailer_price).association(:offer)
  end
end

# Usage
ProductSerializer.new(product).serializable_hash(view: :index)
ProductSerializer.new(product).serializable_hash(namespace: :admin, view: :index)

Key Features

  • Single Serializer, Multiple Views: Define different output formats in one class
  • Namespace Support: Create context-specific serialization (admin, user, mobile, web)
  • Flexible Attributes: Simple, conditional, and custom attribute definitions
  • Association Handling: Support for both one and many relationships
  • Custom Formatters: Global formatters for consistent data formatting
  • Meta Information: Add pagination and other metadata with root key support

Core Benefits

  1. Reduced Code Duplication: No need for separate serializer classes for different views
  2. BFF Pattern Ready: Perfect for Backend for Frontend architecture with namespace support
  3. Clean & Maintainable: Centralized serialization logic for better organization

Perfect For

  • API development requiring multiple response formats
  • Backend for Frontend (BFF) implementations
  • Applications with different user roles requiring different data views

Quick Help

Configuration

  • How to use different JSON enconder gem.

Default JSON encoder gem is "json" gem..

require "oj"

FlexiView.json_encoder = Oj

Basic Usage

To create a serializer, include FlexiView::Serializer in your class:

class ProductSerializer
  include FlexiView::Serializer

  attributes :id, :name, :price
end

Defining Attributes

Simple Attributes

Define basic attributes that will be serialized:

attributes :id, :name, :discount

Conditional Attributes

Add conditions to attributes using the if option:

attributes :price, :retail_price, format: :currency, if: -> { object.available? }

Custom Attribute Blocks

Define custom attribute logic using blocks:

attribute :profit_margin do |object|
  "#{object.calculate_profit_margin}%"
end

Formatted Attributes

Apply formatters to attributes:

attributes :price, :retail_price, format: :currency

Custom Formatters

Define custom formatters globally:

FlexiView.formatter :currency do |value, _serializer|
  "$ #{value.to_f}"
end

Associations

Has One Association

one :offer, "OfferSerializer"

Has Many Association

many :assets, "AssetSerializer"

Association with Custom Logic

many :specifications, "SpecificationSerializer" do |product|
  product.specifications.not_archived
end

Views

Views allow you to define different sets of fields and associations for different contexts.

Basic View Definition

view(:index).fields(:id, :name, :price).association(:offer).meta(:pagination)

Multiple Views

view(:index).fields(:id, :name, :price).association(:offer)
view(:show).fields(:id, :name, :price, :retail_price)

View with Associations

view(:index).fields(:id, :name).association(:assets, view: :show)

Namespaces

Namespaces allow you to group views by context (e.g., admin, user, api):

namespace(:admin) do
  view(:index)
    .fields(:id, :name, :price, :available_count, :profit_margin)
    .associations(:specifications, :offer, view: :index)
end

Multiple Namespaces

namespace(:admin) do
  view(:index).fields(:id, :name, :price, :available_count)
  view(:show).fields(:id, :name, :price, :retail_price, :profit_margin)
end

namespace(:user) do
  view(:index).fields(:id, :name, :price)
  view(:show).fields(:id, :name, :price, :retail_price)
end

Meta Information

Add meta information to your serialized output:

meta(:pagination, root_key: "products") do |objects|
  { count: objects.length, page: options[:page] }
end

The root_key parameter specifies which key in the serialized output should contain the meta information.

Serialization

Single Object Serialization

# Default serialization (all attributes)
ProductSerializer.new(Product.objects(1)).serializable_hash

# Using a specific view
ProductSerializer.new(Product.objects(1)).serializable_hash(view: :index)

# Using namespace and view
ProductSerializer.new(Product.objects(1)).serializable_hash(namespace: :admin, view: :index)

Array Serialization

FlexiView can serialize both single objects and arrays of objects:

# Serialize array of products
ProductSerializer.new(Product.all).serializable_hash(view: :index)

Benefits

1. Single Serializer, Multiple Views

No need to create separate serializer classes for different views. Define all variations in one place:

class ProductSerializer
  include FlexiView::Serializer

  attributes :id, :name, :price, :description, :stock

  view(:list).fields(:id, :name, :price)
  view(:detail).fields(:id, :name, :price, :description, :stock)
  view(:minimal).fields(:id, :name)
end

2. Easy BFF (Backend for Frontend) Pattern Implementation

Different namespaces for different frontends:

namespace(:mobile) do
  view(:list).fields(:id, :name, :price)
end

namespace(:web) do
  view(:list).fields(:id, :name, :price, :description)
end

namespace(:admin) do
  view(:list).fields(:id, :name, :price, :stock, :created_at)
end

3. Clean and Maintainable Code

All serialization logic is centralized, making it easier to maintain and understand the different output formats.

Examples

Complete Example

# Define formatters
FlexiView.formatter :currency do |value, _serializer|
  "$ #{value.to_f}"
end

class ProductSerializer
  include FlexiView::Serializer

  attributes :id, :name, :available_count
  attributes :price, :retail_price, format: :currency, if: -> { object.id == 1 }

  attribute :profit_margin do |object|
    "#{object.calculate_profile_margin}%"
  end

  many :specifications, "SpecificationSerializer" do |product|
    product.specifications.not_archived
  end

  one :offer, "OfferSerializer"

  meta(:pagination, root_key: "products") do |objects|
    { count: objects.length, page: options[:page] }
  end

  # Public views
  view(:index).fields(:id, :name, :price).association(:offer).meta(:pagination)
  view(:show).fields(:id, :name, :price, :retail_price)

  # Admin namespace
  namespace(:admin) do
    view(:index)
      .fields(:id, :name, :price, :available_count, :profit_margin)
      .associations(:specifications, :offer, view: :index)
  end
end

class AssetSerializer
  include FlexiView::Serializer

  attributes :id, :name, :image, :type

  view(:show).fields(:id, :name, :image)
end

class OfferSerializer
  include FlexiView::Serializer

  attributes :id, :name, :discount
  many :assets, "AssetSerializer"

  view(:index).fields(:id, :name).association(:assets, view: :show)
end

# Usage examples
product = Product.find(1)

# Default serialization
ProductSerializer.new(product).serializable_hash # HASH
ProductSerializer.new(product).serializable # JSON

# Index view
ProductSerializer.new(product).serializable_hash(view: :index)
ProductSerializer.new(product).serializable(view: :index)

# Admin namespace with index view
ProductSerializer.new(product).serializable_hash(namespace: :admin, view: :index)
ProductSerializer.new(product).serializable(namespace: :admin, view: :index)

Serializer Usage Examples

1. All Attributes and Associations Serialization

ProductSerializer.new(Product.objects(1)).serializable_hash

Output:

{:id=>1,
 :name=>"Name - 1",
 :available_count=>499,
 :price=>"$ 53.0",
 :retail_price=>"$ 86.0",
 :profit_margin=>"40%",
 :specifications=>[{:id=>1, :name=>"SpecName - 1", :image=>"http://test.com/test.png"}, {:id=>2, :name=>"SpecName - 2", :image=>"http://test.com/test.png"}],
 :offer=>
  {:id=>1,
   :name=>"Offer - 1",
   :discount=>18,
   :assets=>
    [{:id=>1, :name=>"Offer - 1", :image=>"http://test.com/test.png", :type=>"image"},
     {:id=>2, :name=>"Offer - 2", :image=>"http://test.com/test.png", :type=>"image"}]}}

2. View Attributes and Associations Serialization

ProductSerializer.new(Product.objects).serializable_hash(view: :index)

Output:

{:pagination=>{:count=>2, :page=>nil},
 "products"=>
  [{:id=>1,
    :name=>"Name - 1",
    :price=>"$ 87.0",
    :offer=>
     {:id=>1,
      :name=>"Offer - 1",
      :discount=>45,
      :assets=>
       [{:id=>1, :name=>"Offer - 1", :image=>"http://test.com/test.png", :type=>"image"},
        {:id=>2, :name=>"Offer - 2", :image=>"http://test.com/test.png", :type=>"image"}]}},
   {:id=>2,
    :name=>"Name - 2",
    :offer=>
     {:id=>1,
      :name=>"Offer - 1",
      :discount=>25,
      :assets=>
       [{:id=>1, :name=>"Offer - 1", :image=>"http://test.com/test.png", :type=>"image"},
        {:id=>2, :name=>"Offer - 2", :image=>"http://test.com/test.png", :type=>"image"}]}}]}

3. Namespace View Attributes and Associations Serialization

ProductSerializer.new(Product.objects).serializable_hash(namespace: :admin, view: :index)

Output:

[{:id=>1,
  :name=>"Name - 1",
  :price=>"$ 75.0",
  :available_count=>680,
  :profit_margin=>"1%",
  :specifications=>[{:id=>1, :name=>"SpecName - 1"}, {:id=>2, :name=>"SpecName - 2"}],
  :offer=>
   {:id=>1,
    :name=>"Offer - 1",
    :assets=>[{:id=>1, :name=>"Offer - 1", :image=>"http://test.com/test.png"}, {:id=>2, :name=>"Offer - 2", :image=>"http://test.com/test.png"}]}},
 {:id=>2,
  :name=>"Name - 2",
  :available_count=>274,
  :profit_margin=>"45%",
  :specifications=>[{:id=>1, :name=>"SpecName - 1"}, {:id=>2, :name=>"SpecName - 2"}],
  :offer=>
   {:id=>1,
    :name=>"Offer - 1",
    :assets=>[{:id=>1, :name=>"Offer - 1", :image=>"http://test.com/test.png"}, {:id=>2, :name=>"Offer - 2", :image=>"http://test.com/test.png"}]}}]

Key Differences in Output:

  1. Default Serialization: Returns all defined attributes and associations for a single object
  2. View Serialization: Returns only the fields specified in the view definition, includes meta information with root key structure
  3. Namespace View Serialization: Returns fields specific to the namespace view, focuses on admin-specific data like available_count and profit_margin

Notice how the view serialization includes pagination meta information at the root level with the actual data nested under the "products" key, while the namespace view returns a clean array structure focused on admin requirements.

Development

After checking out the repo, run bin/setup to install dependencies. Then, run rake test 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/jiren/flexi_view. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the code of conduct.

License

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

Code of Conduct

Everyone interacting in the FlexiView project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the code of conduct.