{"id":13878959,"url":"https://github.com/okuramasafumi/alba","last_synced_at":"2025-05-13T20:08:58.860Z","repository":{"id":38416678,"uuid":"235342165","full_name":"okuramasafumi/alba","owner":"okuramasafumi","description":"Alba is a JSON serializer for Ruby, JRuby and TruffleRuby.","archived":false,"fork":false,"pushed_at":"2025-04-20T13:50:03.000Z","size":1392,"stargazers_count":1012,"open_issues_count":3,"forks_count":50,"subscribers_count":8,"default_branch":"main","last_synced_at":"2025-04-26T03:15:36.750Z","etag":null,"topics":["hacktoberfest","json","json-serialization","json-serializer","performance","presenter","ruby"],"latest_commit_sha":null,"homepage":"https://rubygems.org/gems/alba","language":"Ruby","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/okuramasafumi.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":"CONTRIBUTING.md","funding":null,"license":"LICENSE.txt","code_of_conduct":"CODE_OF_CONDUCT.md","threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":"SECURITY.md","support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null}},"created_at":"2020-01-21T12:53:40.000Z","updated_at":"2025-04-24T15:26:18.000Z","dependencies_parsed_at":"2024-11-06T10:18:03.475Z","dependency_job_id":"eff296cb-192d-4ccc-aefc-c60e4fa77975","html_url":"https://github.com/okuramasafumi/alba","commit_stats":{"total_commits":589,"total_committers":29,"mean_commits":"20.310344827586206","dds":0.1952461799660441,"last_synced_commit":"d7351d6e8bad8fa33cc3329ead0c4dc16d3aa8bf"},"previous_names":[],"tags_count":48,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/okuramasafumi%2Falba","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/okuramasafumi%2Falba/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/okuramasafumi%2Falba/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/okuramasafumi%2Falba/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/okuramasafumi","download_url":"https://codeload.github.com/okuramasafumi/alba/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":250926213,"owners_count":21508917,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["hacktoberfest","json","json-serialization","json-serializer","performance","presenter","ruby"],"created_at":"2024-08-06T08:02:05.406Z","updated_at":"2025-05-13T20:08:58.839Z","avatar_url":"https://github.com/okuramasafumi.png","language":"Ruby","funding_links":[],"categories":["Gems","Ruby","API Builder and Discovery"],"sub_categories":["Performance Optimization","Articles"],"readme":"![alba card](https://raw.githubusercontent.com/okuramasafumi/alba/main/logo/alba-card.png)\n----------\n[![Gem Version](https://badge.fury.io/rb/alba.svg)](https://badge.fury.io/rb/alba)\n[![CI](https://github.com/okuramasafumi/alba/actions/workflows/main.yml/badge.svg)](https://github.com/okuramasafumi/alba/actions/workflows/main.yml)\n[![codecov](https://codecov.io/gh/okuramasafumi/alba/branch/main/graph/badge.svg?token=3D3HEZ5OXT)](https://codecov.io/gh/okuramasafumi/alba)\n[![Maintainability](https://api.codeclimate.com/v1/badges/fdab4cc0de0b9addcfe8/maintainability)](https://codeclimate.com/github/okuramasafumi/alba/maintainability)\n![GitHub code size in bytes](https://img.shields.io/github/languages/code-size/okuramasafumi/alba)\n![GitHub](https://img.shields.io/github/license/okuramasafumi/alba)\n[![Contributor Covenant](https://img.shields.io/badge/Contributor%20Covenant-2.1-4baaaa.svg)](CODE_OF_CONDUCT.md)\n\n# Alba\n\nAlba is a JSON serializer for Ruby, JRuby, and TruffleRuby.\n\n## IMPORTANT NOTICE\n\nBoth version `3.0.0` and `2.4.2` contain important bug fix.\n~~However, version `3.0.0` has some bugs (see https://github.com/okuramasafumi/alba/issues/342).\nUntil they get fixed, it's highly recommended to upgrade to version `2.4.2`.\nDependabot and similar tools might create an automated Pull Request to upgrade to `3.0.0`, so it might be required to upgrade to `2.4.2` manually.~~\nVersion `3.0.1` has been released so Ruby 3 users should upgrade to `3.0.1`.\nFor Ruby 2 users, it's highly recommended to upgrade to `2.4.2`.\nSorry for the inconvenience.\n\n## TL;DR\n\nAlba allows you to do something like below.\n\n```ruby\nclass User\n  attr_accessor :id, :name, :email\n\n  def initialize(id, name, email)\n    @id = id\n    @name = name\n    @email = email\n  end\nend\n\nclass UserResource\n  include Alba::Resource\n\n  root_key :user\n\n  attributes :id, :name\n\n  attribute :name_with_email do |resource|\n    \"#{resource.name}: #{resource.email}\"\n  end\nend\n\nuser = User.new(1, 'Masafumi OKURA', 'masafumi@example.com')\nUserResource.new(user).serialize\n# =\u003e '{\"user\":{\"id\":1,\"name\":\"Masafumi OKURA\",\"name_with_email\":\"Masafumi OKURA: masafumi@example.com\"}}'\n```\n\nSeems useful? Continue reading!\n\n## Discussions\n\nAlba uses [GitHub Discussions](https://github.com/okuramasafumi/alba/discussions) to openly discuss the project.\n\nIf you've already used Alba, please consider posting your thoughts and feelings on [Feedback](https://github.com/okuramasafumi/alba/discussions/categories/feedback). The fact that you enjoy using Alba gives me energy to keep developing Alba!\n\nIf you have feature requests or interesting ideas, join us with [Ideas](https://github.com/okuramasafumi/alba/discussions/categories/ideas). Let's make Alba even better, together!\n\n## Resources\n\nIf you want to know more about Alba, there's a [screencast](https://hanamimastery.com/episodes/21-serialization-with-alba) created by Sebastian from [Hanami Mastery](https://hanamimastery.com/). It covers basic features of Alba and how to use it in Hanami.\n\n## What users say about Alba\n\n\u003e Alba is a well-maintained JSON serialization engine, for Ruby, JRuby, and TruffleRuby implementations, and what I like in this gem - except of its speed, is the easiness of use, no dependencies and the fact it plays well with any Ruby application!\n\n[Hanami Mastery by Seb Wilgosz](https://hanamimastery.com/episodes/21-serialization-with-alba)\n\n\u003e Alba is more feature-rich and pretty fast, too\n\n[Gemfile of dreams by Evil Martians](https://evilmartians.com/chronicles/gemfile-of-dreams-libraries-we-use-to-build-rails-apps)\n\n## Why Alba?\n\nBecause it's fast, easy and feature rich!\n\n### Fast\n\nAlba is faster than most of the alternatives. We have a [benchmark](https://github.com/okuramasafumi/alba/tree/main/benchmark).\n\n### Easy\n\nAlba is easy to use because there are only a few methods to remember. It's also easy to understand due to clean and small codebase. Finally it's easy to extend since it provides some methods for override to change default behavior of Alba.\n\n### Feature rich\n\nWhile Alba's core is simple, it provides additional features when you need them. For example, Alba provides [a way to control circular associations](#circular-associations-control), [root key and association resource name inference](#root-key-and-association-resource-name-inference) and [supports layouts](#layout).\n\n### Other reasons\n\n- Dependency free, no need to install `oj` or `activesupport` while Alba works well with them\n- Well tested, the test coverage is 99%\n- Well maintained, getting frequent update and new releases (see [version history](https://rubygems.org/gems/alba/versions))\n\n## Installation\n\nAdd this line to your application's Gemfile:\n\n```ruby\ngem 'alba'\n```\n\nAnd then execute:\n\n    $ bundle install\n\nOr install it yourself as:\n\n    $ gem install alba\n\n## Supported Ruby versions\n\nAlba supports CRuby 3.0 and higher and latest JRuby and TruffleRuby.\n\n## Documentation\n\nYou can find the documentation on [GitHub Pages](https://okuramasafumi.github.io/alba/).\n\n## Features\n\n* Conditional attributes and associations\n* Selectable backend\n* Key transformation\n* Root key and association resource name inference\n* Inline definition without explicit classes\n* Error handling\n* Nil handling\n* Circular associations control\n* Types for validation and conversion\n* Layout\n* No runtime dependencies\n\n## Usage\n\n### Configuration\n\nAlba's configuration is fairly simple.\n\n#### Backend configuration\n\nBackend is the actual part serializing an object into JSON. Alba supports these backends.\n\n|name|description|requires_external_gem| encoder|\n|--|--|--|--|\n|`oj`, `oj_strict`|Using Oj in `strict` mode|Yes(C extension)|`Oj.dump(object, mode: :strict)`|\n|`oj_rails`|It's `oj` but in `rails` mode|Yes(C extension)|`Oj.dump(object, mode: :rails)`|\n|`oj_default`|It's `oj` but respects mode set by users|Yes(C extension)|`Oj.dump(object)`|\n|`active_support`|For Rails compatibility|Yes|`ActiveSupport::JSON.encode(object)`|\n|`default`, `json`|Using `json` gem|No|`JSON.generate(object)`|\n\nYou can set a backend like this:\n\n```ruby\nAlba.backend = :oj\n```\n\nThis is equivalent as:\n\n```ruby\nAlba.encoder = -\u003e(object) { Oj.dump(object, mode: :strict) }\n```\n\n#### Encoder configuration\n\nYou can also set JSON encoder directly with a Proc.\n\n```ruby\nAlba.encoder = -\u003e(object) { JSON.generate(object) }\n```\n\nYou can consider setting a backend with Symbol as a shortcut to set encoder.\n\n#### Inference configuration\n\nYou can enable the inference feature using the `Alba.inflector = SomeInflector` API. For example, in a Rails initializer:\n\n```ruby\nAlba.inflector = :active_support\n```\n\nYou can choose which inflector Alba uses for inference. Possible options are:\n\n- `:active_support` for `ActiveSupport::Inflector`\n- `:dry` for `Dry::Inflector`\n- any object which conforms to the protocol (see [below](#custom-inflector))\n\nTo disable inference, set the `inflector` to `nil`:\n\n```ruby\nAlba.inflector = nil\n```\n\nTo check if inference is enabled etc, inspect the return value of `inflector`:\n\n```ruby\nif Alba.inflector.nil?\n  puts 'inflector not set'\nelse\n  puts \"inflector is set to #{Alba.inflector}\"\nend\n```\n\n### Naming\n\nAlba tries to infer resource name from class name like the following.\n\n|Class name|Resource name|\n| --- | --- |\n| FooResource | Foo |\n| FooSerializer | Foo |\n| FooElse | FooElse |\n\nResource name is used as the default name of the root key, so you might want to name it ending with \"Resource\" or \"Serializer\"\n\nWhen you use Alba with Rails, it's recommended to put your resource/serializer classes in corresponding directory such as `app/resources` or `app/serializers`.\n\n### Simple serialization with root key\n\nYou can define attributes with (yes) `attributes` macro with attribute names. If your attribute need some calculations, you can use `attribute` with block.\n\n```ruby\nclass User\n  attr_accessor :id, :name, :email, :created_at, :updated_at\n\n  def initialize(id, name, email)\n    @id = id\n    @name = name\n    @email = email\n    @created_at = Time.now\n    @updated_at = Time.now\n  end\nend\n\nclass UserResource\n  include Alba::Resource\n\n  root_key :user\n\n  attributes :id, :name\n\n  attribute :name_with_email do |resource|\n    \"#{resource.name}: #{resource.email}\"\n  end\nend\n\nuser = User.new(1, 'Masafumi OKURA', 'masafumi@example.com')\nUserResource.new(user).serialize\n# =\u003e '{\"user\":{\"id\":1,\"name\":\"Masafumi OKURA\",\"name_with_email\":\"Masafumi OKURA: masafumi@example.com\"}}'\n```\n\nYou can define instance methods on resources so that you can use it as attribute name in `attributes`.\n\n```ruby\n# The serialization result is the same as above\nclass UserResource\n  include Alba::Resource\n\n  root_key :user, :users # Later is for plural\n\n  attributes :id, :name, :name_with_email\n\n  # Attribute methods must accept one argument for each serialized object\n  def name_with_email(user)\n    \"#{user.name}: #{user.email}\"\n  end\nend\n```\n\nThis even works with users collection.\n\n```ruby\nuser1 = User.new(1, 'Masafumi OKURA', 'masafumi@example.com')\nuser2 = User.new(2, 'Test User', 'test@example.com')\nUserResource.new([user1, user2]).serialize\n# =\u003e '{\"users\":[{\"id\":1,\"name\":\"Masafumi OKURA\",\"name_with_email\":\"Masafumi OKURA: masafumi@example.com\"},{\"id\":2,\"name\":\"Test User\",\"name_with_email\":\"Test User: test@example.com\"}]}'\n```\n\nIf you have a simple case where you want to change only the name, you can use the Symbol to Proc shortcut:\n\n```ruby\nclass UserResource\n  include Alba::Resource\n\n  attribute :some_other_name, \u0026:name\nend\n```\n\n#### Methods conflict\n\nConsider following code:\n\n```ruby\nclass Foo\n  def bar\n    'This is Foo'\n  end\nend\n\nclass FooResource\n  include Alba::Resource\n\n  attributes :bar\n\n  def bar\n    'This is FooResource'\n  end\nend\n\nFooResource.new(Foo.new).serialize\n```\n\nBy default, Alba creates the JSON as `'{\"bar\":\"This is FooResource\"}'`. This means Alba calls a method on a Resource class and doesn't call a method on a target object. This rule is applied to methods that are explicitly defined on Resource class, so methods that Resource class inherits from `Object` class such as `format` are ignored.\n\n```ruby\nclass Foo\n  def format\n    'This is Foo'\n  end\nend\n\nclass FooResource\n  include Alba::Resource\n\n  attributes :bar\n\n  # Here, `format` method is available\nend\n\nFooResource.new(Foo.new).serialize\n# =\u003e '{\"bar\":\"This is Foo\"}'\n```\n\nIf you'd like Alba to call methods on a target object, use `prefer_object_method!` like below.\n\n```ruby\nclass Foo\n  def bar\n    'This is Foo'\n  end\nend\n\nclass FooResource\n  include Alba::Resource\n\n  prefer_object_method! # \u003c- important\n\n  attributes :bar\n\n  # This is not called\n  def bar\n    'This is FooResource'\n  end\nend\n\nFooResource.new(Foo.new).serialize\n# =\u003e '{\"bar\":\"This is Foo\"}'\n```\n\n#### Params\n\nYou can pass a Hash to the resource for internal use. It can be used as \"flags\" to control attribute content.\n\n```ruby\nclass UserResource\n  include Alba::Resource\n  attribute :name do |user|\n    params[:upcase] ? user.name.upcase : user.name\n  end\nend\n\nuser = User.new(1, 'Masa', 'test@example.com')\nUserResource.new(user).serialize # =\u003e '{\"name\":\"Masa\"}'\nUserResource.new(user, params: {upcase: true}).serialize # =\u003e '{\"name\":\"MASA\"}'\n```\n\n### Serialization with associations\n\nAssociations can be defined using the `association` macro, which is also aliased as `one`, `many`, `has_one`, and `has_many` for convenience.\n\n```ruby\nclass User\n  attr_reader :id, :created_at, :updated_at\n  attr_accessor :articles\n\n  def initialize(id)\n    @id = id\n    @created_at = Time.now\n    @updated_at = Time.now\n    @articles = []\n  end\nend\n\nclass Article\n  attr_accessor :user_id, :title, :body\n\n  def initialize(user_id, title, body)\n    @user_id = user_id\n    @title = title\n    @body = body\n  end\nend\n\nclass ArticleResource\n  include Alba::Resource\n\n  attributes :title\nend\n\nclass UserResource\n  include Alba::Resource\n\n  attributes :id\n\n  many :articles, resource: ArticleResource\nend\n\nuser = User.new(1)\narticle1 = Article.new(1, 'Hello World!', 'Hello World!!!')\nuser.articles \u003c\u003c article1\narticle2 = Article.new(2, 'Super nice', 'Really nice!')\nuser.articles \u003c\u003c article2\n\nUserResource.new(user).serialize\n# =\u003e '{\"id\":1,\"articles\":[{\"title\":\"Hello World!\"},{\"title\":\"Super nice\"}]}'\n```\n\nYou can define associations inline if you don't need a class for association.\n\n```ruby\nclass ArticleResource\n  include Alba::Resource\n\n  attributes :title\nend\n\nclass UserResource\n  include Alba::Resource\n\n  attributes :id\n\n  many :articles, resource: ArticleResource\nend\n\n# This class works the same as `UserResource`\nclass AnotherUserResource\n  include Alba::Resource\n\n  attributes :id\n\n  many :articles do\n    attributes :title\n  end\nend\n```\n\nYou can \"filter\" association using second proc argument. This proc takes association object, `params` and initial object.\n\nThis feature is useful when you want to modify association, such as adding `includes` or `order` to ActiveRecord relations.\n\n```ruby\nclass User\n  attr_reader :id, :banned\n  attr_accessor :articles\n\n  def initialize(id, banned = false)\n    @id = id\n    @banned = banned\n    @articles = []\n  end\nend\n\nclass Article\n  attr_accessor :id, :title, :body\n\n  def initialize(id, title, body)\n    @id = id\n    @title = title\n    @body = body\n  end\nend\n\nclass ArticleResource\n  include Alba::Resource\n\n  attributes :title\nend\n\nclass UserResource\n  include Alba::Resource\n\n  attributes :id\n\n  # Second proc works as a filter\n  many :articles,\n       proc { |articles, params, user|\n         filter = params[:filter] || :odd?\n         articles.select { |a| a.id.__send__(filter) \u0026\u0026 !user.banned }\n       },\n       resource: ArticleResource\nend\n\nuser = User.new(1)\narticle1 = Article.new(1, 'Hello World!', 'Hello World!!!')\nuser.articles \u003c\u003c article1\narticle2 = Article.new(2, 'Super nice', 'Really nice!')\nuser.articles \u003c\u003c article2\n\nUserResource.new(user).serialize\n# =\u003e '{\"id\":1,\"articles\":[{\"title\":\"Hello World!\"}]}'\nUserResource.new(user, params: {filter: :even?}).serialize\n# =\u003e '{\"id\":1,\"articles\":[{\"title\":\"Super nice\"}]}'\n```\n\nYou can change a key for association with `key` option.\n\n```ruby\nclass UserResource\n  include Alba::Resource\n\n  attributes :id\n\n  many :articles,\n       key: 'my_articles', # Set key here\n       resource: ArticleResource\nend\nUserResource.new(user).serialize\n# =\u003e '{\"id\":1,\"my_articles\":[{\"title\":\"Hello World!\"}]}'\n```\n\nYou can omit the resource option if you enable Alba's [inference](#inference-configuration) feature.\n\n```ruby\nAlba.inflector = :active_support\n\nclass UserResource\n  include Alba::Resource\n\n  attributes :id\n\n  many :articles # Using `ArticleResource`\nend\nUserResource.new(user).serialize\n# =\u003e '{\"id\":1,\"my_articles\":[{\"title\":\"Hello World!\"}]}'\n```\n\nIf you need complex logic to determine what resource to use for association, you can use a Proc for resource option.\n\n```ruby\nclass UserResource\n  include Alba::Resource\n\n  attributes :id\n\n  many :articles, resource: -\u003e(article) { article.with_comment? ? ArticleWithCommentResource : ArticleResource }\nend\n```\n\nNote that using a Proc slows down serialization if there are too `many` associated objects.\n\n#### Params override\n\nAssociations can override params. This is useful when associations are deeply nested.\n\n```ruby\nclass BazResource\n  include Alba::Resource\n\n  attributes :data\n  attributes :secret, if: proc { params[:expose_secret] }\nend\n\nclass BarResource\n  include Alba::Resource\n\n  one :baz, resource: BazResource\nend\n\nclass FooResource\n  include Alba::Resource\n\n  root_key :foo\n\n  one :bar, resource: BarResource\nend\n\nclass FooResourceWithParamsOverride\n  include Alba::Resource\n\n  root_key :foo\n\n  one :bar, resource: BarResource, params: {expose_secret: false}\nend\n\nBaz = Struct.new(:data, :secret)\nBar = Struct.new(:baz)\nFoo = Struct.new(:bar)\n\nfoo = Foo.new(Bar.new(Baz.new(1, 'secret')))\nFooResource.new(foo, params: {expose_secret: true}).serialize # =\u003e '{\"foo\":{\"bar\":{\"baz\":{\"data\":1,\"secret\":\"secret\"}}}}'\nFooResourceWithParamsOverride.new(foo, params: {expose_secret: true}).serialize # =\u003e '{\"foo\":{\"bar\":{\"baz\":{\"data\":1}}}}'\n```\n\n### Nested Attribute\n\nAlba supports nested attributes that makes it easy to build complex data structure from single object.\n\nIn order to define nested attributes, you can use `nested` or `nested_attribute` (alias of `nested`).\n\n```ruby\nclass User\n  attr_accessor :id, :name, :email, :city, :zipcode\n\n  def initialize(id, name, email, city, zipcode)\n    @id = id\n    @name = name\n    @email = email\n    @city = city\n    @zipcode = zipcode\n  end\nend\n\nclass UserResource\n  include Alba::Resource\n\n  root_key :user\n\n  attributes :id\n\n  nested_attribute :address do\n    attributes :city, :zipcode\n  end\nend\n\nuser = User.new(1, 'Masafumi OKURA', 'masafumi@example.com', 'Tokyo', '0000000')\nUserResource.new(user).serialize\n# =\u003e '{\"user\":{\"id\":1,\"address\":{\"city\":\"Tokyo\",\"zipcode\":\"0000000\"}}}'\n```\n\nNested attributes can be nested deeply.\n\n```ruby\nclass FooResource\n  include Alba::Resource\n\n  root_key :foo\n\n  nested :bar do\n    nested :baz do\n      attribute :deep do\n        42\n      end\n    end\n  end\nend\n\nFooResource.new(nil).serialize\n# =\u003e '{\"foo\":{\"bar\":{\"baz\":{\"deep\":42}}}}'\n```\n\n### Inline definition with `Alba.serialize`\n\n`Alba.serialize` method is a shortcut to define everything inline.\n\n```ruby\nAlba.serialize(user, root_key: :foo) do\n  attributes :id\n  many :articles do\n    attributes :title, :body\n  end\nend\n# =\u003e '{\"foo\":{\"id\":1,\"articles\":[{\"title\":\"Hello World!\",\"body\":\"Hello World!!!\"},{\"title\":\"Super nice\",\"body\":\"Really nice!\"}]}}'\n```\n\n`Alba.serialize` can be used when you don't know what kind of object you serialize. For example:\n\n```ruby\nAlba.serialize(something)\n# =\u003e Same as `FooResource.new(something).serialize` when `something` is an instance of `Foo`.\n```\n\nAlthough this might be useful sometimes, it's generally recommended to define a class for Resource. Defining a class is often more readable and more maintainable, and inline definitions cannot levarage the benefit of YJIT (it's the slowest with the benchmark YJIT enabled).\n\n#### Inline definition for multiple root keys\n\nWhile Alba doesn't directly support multiple root keys, you can simulate it with `Alba.serialize`.\n\n```ruby\n# Define foo and bar local variables here\n\nAlba.serialize do\n  attribute :key1 do\n    FooResource.new(foo).to_h\n  end\n\n  attribute :key2 do\n    BarResource.new(bar).to_h\n  end\nend\n# =\u003e JSON containing \"key1\" and \"key2\" as root keys\n```\n\nNote that we must use `to_h`, not `serialize`, with resources.\n\nWe can also generate a JSON with multiple root keys without making any class by the combination of `Alba.serialize` and `Alba.hashify`.\n\n```ruby\n# Define foo and bar local variables here\n\nAlba.serialize do\n  attribute :foo do\n    Alba.hashify(foo) do\n      attributes :id, :name # For example\n    end\n  end\n\n  attribute :bar do\n    Alba.hashify(bar) do\n      attributes :id\n    end\n  end\nend\n# =\u003e JSON containing \"foo\" and \"bar\" as root keys\n```\n\n#### Inline definition with heterogeneous collection\n\nAlba allows to serialize a heterogeneous collection with `Alba.serialize`.\n\n```ruby\nFoo = Data.define(:id, :name)\nBar = Data.define(:id, :address)\n\nclass FooResource\n  include Alba::Resource\n\n  attributes :id, :name\nend\n\nclass BarResource\n  include Alba::Resource\n\n  attributes :id, :address\nend\n\nclass CustomFooResource\n  include Alba::Resource\n\n  attributes :id\nend\n\nfoo1 = Foo.new(1, 'foo1')\nfoo2 = Foo.new(2, 'foo2')\nbar1 = Bar.new(1, 'bar1')\nbar2 = Bar.new(2, 'bar2')\n\n# This works only when inflector is set\nAlba.serialize([foo1, bar1, foo2, bar2], with: :inference)\n# =\u003e '[{\"id\":1,\"name\":\"foo1\"},{\"id\":1,\"address\":\"bar1\"},{\"id\":2,\"name\":\"foo2\"},{\"id\":2,\"address\":\"bar2\"}]'\n\nAlba.serialize(\n  [foo1, bar1, foo2, bar2],\n  # `with` option takes a lambda to return resource class\n  with: lambda do |obj|\n          case obj\n          when Foo\n            CustomFooResource\n          when Bar\n            BarResource\n          else\n            raise # Impossible in this case\n          end\n        end\n)\n# =\u003e '[{\"id\":1},{\"id\":1,\"address\":\"bar1\"},{\"id\":2},{\"id\":2,\"address\":\"bar2\"}]'\n# Note `CustomFooResource` is used here\n\n```\n\n### Serializable Hash\n\nInstead of serializing to JSON, you can also output a Hash by calling `serializable_hash` or the `to_h` alias. Note also that the `serialize` method is aliased as `to_json`.\n\n```ruby\n# These are equivalent and will return serialized JSON\nUserResource.new(user).serialize\nUserResource.new(user).to_json\n\n# These are equivalent and will return a Hash\nUserResource.new(user).serializable_hash\nUserResource.new(user).to_h\n```\n\nIf you want a Hash that corresponds to a JSON String returned by `serialize` method, you can use `as_json`.\n\n```ruby\n# These are equivalent and will return the same result\nUserResource.new(user).serialize\nUserResource.new(user).to_json\nJSON.generate(UserResource.new(user).as_json)\n```\n\n### Inheritance\n\nWhen you include `Alba::Resource` in your class, it's just a class so you can define any class that inherits from it. You can add new attributes to inherited class like below:\n\n```ruby\nclass FooResource\n  include Alba::Resource\n\n  root_key :foo\n\n  attributes :bar\nend\n\nclass ExtendedFooResource \u003c FooResource\n  root_key :foofoo\n\n  attributes :baz\nend\n\nFoo = Struct.new(:bar, :baz)\nfoo = Foo.new(1, 2)\nFooResource.new(foo).serialize # =\u003e '{\"foo\":{\"bar\":1}}'\nExtendedFooResource.new(foo).serialize # =\u003e '{\"foofoo\":{\"bar\":1,\"baz\":2}}'\n```\n\nIn this example we add `baz` attribute and change `root_key`. This way, you can extend existing resource classes just like normal OOP. Don't forget that when your inheritance structure is too deep it'll become difficult to modify existing classes.\n\n### Filtering attributes\n\nTo filter attributes, you can use `select` instance method. Using `attributes` instance method is deprecated and will be removed in the future.\n\n#### Filtering attributes with `select`\n\n`select` takes two or three parameters, the name of an attribute, the value of an attribute and the attribute object (`Alba::Association`, for example). If it returns false that attribute is rejected.\n\n```ruby\nclass Foo\n  attr_accessor :id, :name, :body\n\n  def initialize(id, name, body)\n    @id = id\n    @name = name\n    @body = body\n  end\nend\n\nclass GenericFooResource\n  include Alba::Resource\n\n  attributes :id, :name, :body\nend\n\nclass RestrictedFooResource \u003c GenericFooResource\n  def select(_key, value)\n    !value.nil?\n  end\n\n  # This is also possible\n  # def select(_key, _value, _attribute)\nend\n\nfoo = Foo.new(1, nil, 'body')\n\nRestrictedFooResource.new(foo).serialize\n# =\u003e '{\"id\":1,\"body\":\"body\"}'\n```\n\n### Key transformation\n\nIf you have [inference](#inference-configuration) enabled, you can use the `transform_keys` DSL to transform attribute keys.\n\n```ruby\nAlba.inflector = :active_support\n\nclass User\n  attr_reader :id, :first_name, :last_name\n\n  def initialize(id, first_name, last_name)\n    @id = id\n    @first_name = first_name\n    @last_name = last_name\n  end\nend\n\nclass UserResource\n  include Alba::Resource\n\n  attributes :id, :first_name, :last_name\n\n  transform_keys :lower_camel\nend\n\nuser = User.new(1, 'Masafumi', 'Okura')\nUserResourceCamel.new(user).serialize\n# =\u003e '{\"id\":1,\"firstName\":\"Masafumi\",\"lastName\":\"Okura\"}'\n```\n\nPossible values for `transform_keys` argument are:\n\n* `:camel` for CamelCase\n* `:lower_camel` for lowerCamelCase\n* `:dash` for dash-case\n* `:snake` for snake_case\n* `:none` for not transforming keys\n\n#### Root key transformation\n\nYou can also transform root key when:\n\n* `Alba.inflector` is set\n* `root_key!` is called in Resource class\n* `root` option of `transform_keys` is set to true\n\n```ruby\nAlba.inflector = :active_support\n\nclass BankAccount\n  attr_reader :account_number\n\n  def initialize(account_number)\n    @account_number = account_number\n  end\nend\n\nclass BankAccountResource\n  include Alba::Resource\n\n  root_key!\n\n  attributes :account_number\n  transform_keys :dash, root: true\nend\n\nbank_account = BankAccount.new(123_456_789)\nBankAccountResource.new(bank_account).serialize\n# =\u003e '{\"bank-account\":{\"account-number\":123456789}}'\n```\n\nThis is the default behavior from version 2.\n\nFind more details in the [Inference configuration](#inference-configuration) section.\n\n#### Key transformation cascading\n\nWhen you use `transform_keys` with inline association, it automatically applies the same transformation type to those inline association.\n\nThis is the default behavior from version 2, but you can do the same thing with adding `transform_keys` to each association.\n\nYou can also turn it off by setting `cascade: false` option to `transform_keys`.\n\n```ruby\nclass User\n  attr_reader :id, :first_name, :last_name, :bank_account\n\n  def initialize(id, first_name, last_name)\n    @id = id\n    @first_name = first_name\n    @last_name = last_name\n    @bank_account = BankAccount.new(1234)\n  end\nend\n\nclass BankAccount\n  attr_reader :account_number\n\n  def initialize(account_number)\n    @account_number = account_number\n  end\nend\n\nclass UserResource\n  include Alba::Resource\n\n  attributes :id, :first_name, :last_name\n\n  transform_keys :lower_camel # Default is cascade: true\n\n  one :bank_account do\n    attributes :account_number\n  end\nend\n\nuser = User.new(1, 'Masafumi', 'Okura')\nUserResource.new(user).serialize\n# =\u003e '{\"id\":1,\"firstName\":\"Masafumi\",\"lastName\":\"Okura\",\"bankAccount\":{\"accountNumber\":1234}}'\n```\n\n#### Custom inflector\n\nA custom inflector can be plugged in as follows.\n\n```ruby\nmodule CustomInflector\n  module_function\n\n  def camelize(string); end\n\n  def camelize_lower(string); end\n\n  def dasherize(string); end\n\n  def underscore(string); end\n\n  def classify(string); end\nend\n\nAlba.inflector = CustomInflector\n```\n\n### Conditional attributes\n\nFiltering attributes with overriding `attributes` works well for simple cases. However, It's cumbersome when we want to filter various attributes based on different conditions for keys.\n\nIn these cases, conditional attributes works well. We can pass `if` option to `attributes`, `attribute`, `one` and `many`. Below is an example for the same effect as [filtering attributes section](#filtering-attributes).\n\n```ruby\nclass User\n  attr_accessor :id, :name, :email\n\n  def initialize(id, name, email)\n    @id = id\n    @name = name\n    @email = email\n  end\nend\n\nclass UserResource\n  include Alba::Resource\n\n  attributes :id, :name, :email, if: proc { |user, attribute| !attribute.nil? }\nend\n\nuser = User.new(1, nil, nil)\nUserResource.new(user).serialize # =\u003e '{\"id\":1}'\n```\n\n#### Caution for the second parameter in `if` proc\n\n`if` proc takes two parameters. The first one is the target object, `user` in the example above. The second one is `attribute` representing each attribute `if` option affects. Note that it actually calls attribute methods, so you cannot use it to prevent attribute methods called. This means if the target object is an `ActiveRecord::Base` object and using `association` with `if` option, you might want to skip the second parameter so that the SQL query won't be issued.\n\nExample:\n\n```ruby\nclass User \u003c ApplicationRecord\n  has_many :posts\nend\n\nclass Post \u003c ApplicationRecord\n  belongs_to :user\nend\n\nclass UserResource\n  include Alba::Resource\n\n  # Since `_posts` parameter exists, `user.posts` are loaded\n  many :posts, if: proc { |user, _posts| user.admin? }\nend\n\nclass UserResource2\n  include Alba::Resource\n\n  # Since `_posts` parameter doesn't exist, `user.posts` are NOT loaded\n  many :posts, if: proc { |user| user.admin? \u0026\u0026 params[:include_post] }\nend\n```\n\n### Traits\n\nTraits is an easy way to a group of attributes and apply it to the resource.\n\n```ruby\nclass User\n  attr_accessor :id, :name, :email\n\n  def initialize(id, name, email)\n    @id = id\n    @name = name\n    @email = email\n  end\nend\n\nclass UserResource\n  include Alba::Resource\n\n  attributes :id\n\n  trait :additional do\n    attributes :name, :email\n  end\nend\n\nuser = User.new(1, 'Foo', 'foo@example.org')\nUserResource.new(user).serialize # =\u003e '{\"id\":1}'\nUserResource.new(user, with_traits: :additional).serialize # =\u003e '{\"id\":1,\"name\":\"Foo\",\"email\":\"foo@example.com\"}'\n```\n\nThis way, we can keep the resource class simple and inject conditions from outside. We can get the same result with the combination of `if` and `params`, but using `traits` DSL can make the resource class readable.\n\nWe can specify multiple traits at once with `with_traits: []` keyword argument.\n\n### Default\n\nAlba doesn't support default value for attributes, but it's easy to set a default value.\n\n```ruby\nclass FooResource\n  attribute :bar do |foo|\n    foo.bar || 'default bar'\n  end\nend\n```\n\nWe believe this is clearer than using some (not implemented yet) DSL such as `default` because there are some conditions where default values should be applied (`nil`, `blank?`, `empty?` etc.)\n\n### Root key and association resource name inference\n\nIf [inference](#inference-configuration) is enabled, Alba tries to infer the root key and association resource names.\n\n```ruby\nAlba.inflector = :active_support\n\nclass User\n  attr_reader :id\n  attr_accessor :articles\n\n  def initialize(id)\n    @id = id\n    @articles = []\n  end\nend\n\nclass Article\n  attr_accessor :id, :title\n\n  def initialize(id, title)\n    @id = id\n    @title = title\n  end\nend\n\nclass ArticleResource\n  include Alba::Resource\n\n  attributes :title\nend\n\nclass UserResource\n  include Alba::Resource\n\n  root_key! # This is required to add inferred root key, otherwise it has no root key\n\n  attributes :id\n\n  many :articles\nend\n\nuser = User.new(1)\nuser.articles \u003c\u003c Article.new(1, 'The title')\n\nUserResource.new(user).serialize # =\u003e '{\"user\":{\"id\":1,\"articles\":[{\"title\":\"The title\"}]}}'\nUserResource.new([user]).serialize # =\u003e '{\"users\":[{\"id\":1,\"articles\":[{\"title\":\"The title\"}]}]}'\n```\n\nThis resource automatically sets its root key to either \"users\" or \"user\", depending on the given object is collection or not.\n\nAlso, you don't have to specify which resource class to use with `many`. Alba infers it from association name.\n\nFind more details in the [Inference configuration](#inference-configuration) section.\n\n### Error handling\n\nYou can set error handler globally or per resource using `on_error`.\n\n```ruby\nclass User\n  attr_accessor :id, :name\n\n  def initialize(id, name, email)\n    @id = id\n    @name = name\n    @email = email\n  end\n\n  def email\n    raise 'Error!'\n  end\nend\n\nclass UserResource\n  include Alba::Resource\n\n  attributes :id, :name, :email\n\n  on_error :ignore\nend\n\nuser = User.new(1, 'Test', 'email@example.com')\nUserResource.new(user).serialize # =\u003e '{\"id\":1,\"name\":\"Test\"}'\n```\n\nThis way you can exclude an entry when fetching an attribute gives an exception.\n\nThere are four possible arguments `on_error` method accepts.\n\n* `:raise` re-raises an error. This is the default behavior.\n* `:ignore` ignores the entry with the error.\n* `:nullify` sets the attribute with the error to `nil`.\n* Block gives you more control over what to be returned.\n\nThe block receives five arguments, `error`, `object`, `key`, `attribute` and `resource class` and must return a two-element array. Below is an example.\n\n```ruby\nclass ExampleResource\n  include Alba::Resource\n  on_error do |error, object, key, attribute, resource_class|\n    if resource_class == MyResource\n      ['error_fallback', object.error_fallback]\n    else\n      [key, error.message]\n    end\n  end\nend\n```\n\n### Nil handling\n\nSometimes we want to convert `nil` to different values such as empty string. Alba provides a flexible way to handle `nil`.\n\n```ruby\nclass User\n  attr_reader :id, :name, :age\n\n  def initialize(id, name = nil, age = nil)\n    @id = id\n    @name = name\n    @age = age\n  end\nend\n\nclass UserResource\n  include Alba::Resource\n\n  on_nil { '' }\n\n  root_key :user, :users\n\n  attributes :id, :name, :age\nend\n\nUserResource.new(User.new(1)).serialize\n# =\u003e '{\"user\":{\"id\":1,\"name\":\"\",\"age\":\"\"}}'\n```\n\nYou can get various information via block parameters.\n\n```ruby\nclass UserResource\n  include Alba::Resource\n\n  on_nil do |object, key|\n    if key == 'age'\n      20\n    else\n      \"User#{object.id}\"\n    end\n  end\n\n  root_key :user, :users\n\n  attributes :id, :name, :age\nend\n\nUserResource.new(User.new(1)).serialize\n# =\u003e '{\"user\":{\"id\":1,\"name\":\"User1\",\"age\":20}}'\n```\n\nNote that `on_nil` does NOT work when the given object itself is `nil`. There are a few possible ways to deal with `nil`.\n\n- Use `if` statement and avoid using Alba when the object is `nil`\n- Use \"Null Object\" pattern\n\n### Metadata\n\nYou can set a metadata with `meta` DSL or `meta` option.\n\n```ruby\nclass UserResource\n  include Alba::Resource\n\n  root_key :user, :users\n\n  attributes :id, :name\n\n  meta do\n    if object.is_a?(Enumerable)\n      {size: object.size}\n    else\n      {foo: :bar}\n    end\n  end\nend\n\nuser = User.new(1, 'Masafumi OKURA', 'masafumi@example.com')\nUserResource.new([user]).serialize\n# =\u003e '{\"users\":[{\"id\":1,\"name\":\"Masafumi OKURA\"}],\"meta\":{\"size\":1}}'\n\n# You can merge metadata with `meta` option\n\nUserResource.new([user]).serialize(meta: {foo: :bar})\n# =\u003e '{\"users\":[{\"id\":1,\"name\":\"Masafumi OKURA\"}],\"meta\":{\"size\":1,\"foo\":\"bar\"}}'\n```\n\nYou can change the key for metadata. If you change the key, it also affects the key when you pass `meta` option.\n\n```ruby\n# You can change meta key\nclass UserResourceWithDifferentMetaKey\n  include Alba::Resource\n\n  root_key :user, :users\n\n  attributes :id, :name\n\n  meta :my_meta do\n    {foo: :bar}\n  end\nend\n\nUserResourceWithDifferentMetaKey.new([user]).serialize\n# =\u003e '{\"users\":[{\"id\":1,\"name\":\"Masafumi OKURA\"}],\"my_meta\":{\"foo\":\"bar\"}}'\n\nUserResourceWithDifferentMetaKey.new([user]).serialize(meta: {extra: 42})\n# =\u003e '{\"users\":[{\"id\":1,\"name\":\"Masafumi OKURA\"}],\"my_meta\":{\"foo\":\"bar\",\"extra\":42}}'\n\nclass UserResourceChangingMetaKeyOnly\n  include Alba::Resource\n\n  root_key :user, :users\n\n  attributes :id, :name\n\n  meta :my_meta\nend\n\nUserResourceChangingMetaKeyOnly.new([user]).serialize\n# =\u003e '{\"users\":[{\"id\":1,\"name\":\"Masafumi OKURA\"}]}'\n\nUserResourceChangingMetaKeyOnly.new([user]).serialize(meta: {extra: 42})\n# =\u003e '{\"users\":[{\"id\":1,\"name\":\"Masafumi OKURA\"}],\"my_meta\":{\"extra\":42}}'\n```\n\nIt's also possible to remove the key for metadata, resulting a flat structure.\n\n```ruby\nclass UserResourceRemovingMetaKey\n  include Alba::Resource\n\n  root_key :user, :users\n\n  attributes :id, :name\n\n  meta nil\nend\n\nUserResourceRemovingMetaKey.new([user]).serialize\n# =\u003e '{\"users\":[{\"id\":1,\"name\":\"Masafumi OKURA\"}]}'\n\nUserResourceRemovingMetaKey.new([user]).serialize(meta: {extra: 42})\n# =\u003e '{\"users\":[{\"id\":1,\"name\":\"Masafumi OKURA\"}],\"extra\":42}'\n\n# You can set metadata with `meta` option alone\n\nclass UserResourceWithoutMeta\n  include Alba::Resource\n\n  root_key :user, :users\n\n  attributes :id, :name\nend\n\nUserResourceWithoutMeta.new([user]).serialize(meta: {foo: :bar})\n# =\u003e '{\"users\":[{\"id\":1,\"name\":\"Masafumi OKURA\"}],\"meta\":{\"foo\":\"bar\"}}'\n```\n\nYou can use `object` method to access the underlying object and `params` to access the params in `meta` block.\n\nNote that setting root key is required when setting a metadata.\n\n### Circular associations control\n\n**Note that this feature works correctly since version 1.3. In previous versions it doesn't work as expected.**\n\nYou can control circular associations with `within` option. `within` option is a nested Hash such as `{book: {authors: books}}`. In this example, Alba serializes a book's authors' books. This means you can reference `BookResource` from `AuthorResource` and vice versa. This is really powerful when you have a complex data structure and serialize certain parts of it.\n\nFor more details, please refer to [test code](https://github.com/okuramasafumi/alba/blob/main/test/usecases/circular_association_test.rb)\n\n### Types\n\nYou can validate and convert input with types.\n\n```ruby\nclass User\n  attr_reader :id, :name, :age, :bio, :admin, :created_at\n\n  def initialize(id, name, age, bio = '', admin = false)\n    @id = id\n    @name = name\n    @age = age\n    @admin = admin\n    @bio = bio\n    @created_at = Time.new(2020, 10, 10)\n  end\nend\n\nclass UserResource\n  include Alba::Resource\n\n  attributes :name, id: [String, true], age: [Integer, true], bio: String, admin: [:Boolean, true], created_at: [String, -\u003e(object) { object.strftime('%F') }]\nend\n\nuser = User.new(1, 'Masafumi OKURA', '32', 'Ruby dev')\nUserResource.new(user).serialize\n# =\u003e '{\"name\":\"Masafumi OKURA\",\"id\":\"1\",\"age\":32,\"bio\":\"Ruby dev\",\"admin\":false,\"created_at\":\"2020-10-10\"}'\n```\n\nNotice that `id` and `created_at` are converted to String and `age` is converted to Integer.\n\nIf type is not correct and auto conversion is disabled (default), `TypeError` occurs.\n\n```ruby\nuser = User.new(1, 'Masafumi OKURA', '32', nil) # bio is nil and auto conversion is disabled for bio\nUserResource.new(user).serialize\n# =\u003e TypeError, 'Attribute bio is expected to be String but actually nil.'\n```\n\n#### Custom types\n\nYou can define custom types to abstract data conversion logic. To define custom types, you can use `Alba.register_type` like below.\n\n```ruby\n# Typically in initializer\nAlba.register_type :iso8601, converter: -\u003e(time) { time.iso8601(3) }, auto_convert: true\n```\n\nThen use it as regular types.\n\n```rb\nclass UserResource\n  include Alba::Resource\n\n  attributes :id, created_at: :iso8601\nend\n```\n\nYou now get `created_at` attribute with `iso8601` format!\n\n#### Generating TypeScript types with typelizer gem\n\nWe often want TypeScript types corresponding to serializers. That's possible with [typelizer](https://github.com/skryukov/typelizer) gem.\n\nFor more information, please read its README.\n\n### Collection serialization into Hash\n\nSometimes we want to serialize a collection into a Hash, not an Array. It's possible with Alba.\n\n```ruby\nclass User\n  attr_reader :id, :name\n\n  def initialize(id, name)\n    @id = id\n    @name = name\n  end\nend\n\nclass UserResource\n  include Alba::Resource\n\n  collection_key :id # This line is important\n\n  attributes :id, :name\nend\n\nuser1 = User.new(1, 'John')\nuser2 = User.new(2, 'Masafumi')\n\nUserResource.new([user1, user2]).serialize\n# =\u003e '{\"1\":{\"id\":1,\"name\":\"John\"},\"2\":{\"id\":2,\"name\":\"Masafumi\"}}'\n```\n\nIn the snippet above, `collection_key :id` specifies the key used for the key of the collection hash. In this example it's `:id`.\n\nMake sure that collection key is unique for the collection.\n\n### Layout\n\nSometimes we'd like to serialize JSON into a template. In other words, we need some structure OUTSIDE OF serialized JSON. IN HTML world, we call it a \"layout\".\n\nAlba supports serializing JSON in a layout. You need a file for layout and then to specify file with `layout` method.\n\n```erb\n{\n  \"header\": \"my_header\",\n  \"body\": \u003c%= serialized_json %\u003e\n}\n```\n\n```ruby\nclass FooResource\n  include Alba::Resource\n  layout file: 'my_layout.json.erb'\nend\n```\n\nNote that layout files are treated as `json` and `erb` and evaluated in a context of the resource, meaning\n\n* A layout file must be a valid JSON\n* You must write `\u003c%= serialized_json %\u003e` in a layout to put serialized JSON string into a layout\n* You can access `params` in a layout so that you can add virtually any objects to a layout\n  * When you access `params`, it's usually a Hash. You can use `encode` method in a layout to convert `params` Hash into a JSON with the backend you use\n* You can also access `object`, the underlying object for the resource\n\nIn case you don't want to have a file for layout, Alba lets you define and apply layouts inline:\n\n```ruby\nclass FooResource\n  include Alba::Resource\n  layout inline: proc {\n    {\n      header: 'my header',\n      body: serializable_hash\n    }\n  }\nend\n```\n\nIn the example above, we specify a Proc which returns a Hash as an inline layout. In the Proc we can use `serializable_hash` method to access a Hash right before serialization.\n\nYou can also use a Proc which returns String, not a Hash, for an inline layout.\n\n```ruby\nclass FooResource\n  include Alba::Resource\n  layout inline: proc {\n    %({\n      \"header\": \"my header\",\n      \"body\": #{serialized_json}\n    })\n  }\nend\n```\n\nIt looks similar to file layout but you must use string interpolation for method calls since it's not an ERB.\n\nAlso note that we use percentage notation here to use double quotes. Using single quotes in inline string layout causes the error which might be resolved in other ways.\n\n### Helper\n\nInheritance works well in most of the cases to share behaviors. One of the exceptions is when you want to shared behaviors with inline association. For example:\n\n```ruby\nclass ApplicationResource\n  include Alba::Resource\n\n  def self.with_id\n    attributes(:id)\n  end\nend\n\nclass LibraryResource \u003c ApplicationResource\n  with_id\n  attributes :created_at\n\n  with_many :library_books do\n    with_id # This DOES NOT work!\n    attributes :created_at\n  end\nend\n```\n\nThis doesn't work. Technically, inside of `has_many` is a separate class which doesn't inherit from the base class (`LibraryResource` in this example).\n\n`helper` solves this problem. It's just a mark for methods that should be shared with inline associations.\n\n```ruby\nclass ApplicationResource\n  include Alba::Resource\n\n  helper do\n    def with_id\n      attributes(:id)\n    end\n  end\nend\n# Now `LibraryResource` works!\n```\n\nWithin `helper` block, all methods should be defined without `self.`.\n\n### Experimental: modification API\n\nAlba now provides an experimental API to modify existing resource class without adding new classes. Currently only `transform_keys!` is implemented.\n\nModification API returns a new class with given modifications. It's useful when you want lots of resource classes with small changes. See it in action:\n\n```ruby\nclass FooResource\n  include Alba::Resource\n\n  transform_keys :camel\n\n  attributes :id\nend\n\n# Rails app\nclass FoosController \u003c ApplicationController\n  def index\n    foos = Foo.where(some: :condition)\n    key_transformation_type = params[:key_transformation_type] # Say it's \"lower_camel\"\n    # When params is absent, do not use modification API since it's slower\n    resource_class = key_transformation_type ? FooResource.transform_keys!(key_transformation_type) : FooResource\n    render json: resource_class.new(foos).serialize # The keys are lower_camel\n  end\nend\n```\n\nThe point is that there's no need to define classes for each key transformation type (dash, camel, lower_camel and snake). This gives even more flexibility.\n\nThere are some drawbacks with this approach. For example, it creates an internal, anonymous class when it's called, so there is a performance penalty and debugging difficulty. It's recommended to define classes manually when you don't need high flexibility.\n\n### Caching\n\nCurrently, Alba doesn't support caching, primarily due to the behavior of `ActiveRecord::Relation`'s cache. See [the issue](https://github.com/rails/rails/issues/41784).\n\n### Extend Alba\n\nSometimes we have shared behaviors across resources. In such cases we can have a module for common logic.\n\nIn `attribute` block we can call instance method so we can improve the code below:\n\n```ruby\nclass FooResource\n  include Alba::Resource\n  # other attributes\n  attribute :created_at do |foo|\n    foo.created_at.strftime('%m/%d/%Y')\n  end\n\n  attribute :updated_at do |foo|\n    foo.updated_at.strftime('%m/%d/%Y')\n  end\nend\n\nclass BarResource\n  include Alba::Resource\n  # other attributes\n  attribute :created_at do |bar|\n    bar.created_at.strftime('%m/%d/%Y')\n  end\n\n  attribute :updated_at do |bar|\n    bar.updated_at.strftime('%m/%d/%Y')\n  end\nend\n```\n\nto:\n\n```ruby\nmodule SharedLogic\n  def format_time(time)\n    time.strftime('%m/%d/%Y')\n  end\nend\n\nclass FooResource\n  include Alba::Resource\n  include SharedLogic\n  # other attributes\n  attribute :created_at do |foo|\n    format_time(foo.created_at)\n  end\n\n  attribute :updated_at do |foo|\n    format_time(foo.updated_at)\n  end\nend\n\nclass BarResource\n  include Alba::Resource\n  include SharedLogic\n  # other attributes\n  attribute :created_at do |bar|\n    format_time(bar.created_at)\n  end\n\n  attribute :updated_at do |bar|\n    format_time(bar.updated_at)\n  end\nend\n```\n\nWe can even add our own DSL to serialize attributes for readability and removing code duplications.\n\nTo do so, we need to `extend` our module. Let's see how we can achieve the same goal with this approach.\n\n```ruby\nmodule AlbaExtension\n  # Here attrs are an Array of Symbol\n  def formatted_time_attributes(*attrs)\n    attrs.each do |attr|\n      attribute(attr) do |object|\n        time = object.__send__(attr)\n        time.strftime('%m/%d/%Y')\n      end\n    end\n  end\nend\n\nclass FooResource\n  include Alba::Resource\n  extend AlbaExtension\n  # other attributes\n  formatted_time_attributes :created_at, :updated_at\nend\n\nclass BarResource\n  include Alba::Resource\n  extend AlbaExtension\n  # other attributes\n  formatted_time_attributes :created_at, :updated_at\nend\n```\n\nIn this way we have shorter and cleaner code. Note that we need to use `send` or `public_send` in `attribute` block to get attribute data.\n\n#### Using `helper`\n\nWhen we `extend AlbaExtension` like above, it's not available in inline associations.\n\n```ruby\nclass BarResource\n  include Alba::Resource\n  extend AlbaExtension\n  # other attributes\n  formatted_time_attributes :created_at, :updated_at\n\n  one :something do\n    # This DOES NOT work!\n    formatted_time_attributes :updated_at\n  end\nend\n```\n\nIn this case, we can use [helper](#helper) instead of `extend`.\n\n```ruby\nclass BarResource\n  include Alba::Resource\n  helper AlbaExtension # HERE!\n  # other attributes\n  formatted_time_attributes :created_at, :updated_at\n\n  one :something do\n    # This WORKS!\n    formatted_time_attributes :updated_at\n  end\nend\n```\n\nYou can also pass options to your helpers.\n\n```ruby\nmodule AlbaExtension\n  def time_attributes(*attrs, **options)\n    attrs.each do |attr|\n      attribute(attr, **options) do |object|\n        object.__send__(attr).iso8601\n      end\n    end\n  end\nend\n```\n\n### Debugging\n\nDebugging is not easy. If you find Alba not working as you expect, there are a few things to do:\n\n1. Inspect\n\nThe typical code looks like this:\n\n```ruby\nclass FooResource\n  include Alba::Resource\n  attributes :id\nend\nFooResource.new(foo).serialize\n```\n\nNotice that we instantiate `FooResource` and then call `serialize` method. We can get various information by calling `inspect` method on it.\n\n```ruby\nputs FooResource.new(foo).inspect # or: p class FooResource.new(foo)\n# =\u003e \"#\u003cFooResource:0x000000010e21f408 @object=[#\u003cFoo:0x000000010e3470d8 @id=1\u003e], @params={}, @within=#\u003cObject:0x000000010df2eac8\u003e, @method_existence={}, @_attributes={:id=\u003e:id}, @_key=nil, @_key_for_collection=nil, @_meta=nil, @_transform_type=:none, @_transforming_root_key=false, @_on_error=nil, @_on_nil=nil, @_layout=nil, @_collection_key=nil\u003e\"\n```\n\nThe output might be different depending on the version of Alba or the object you give, but the concepts are the same. `@object` represents the object you gave as an argument to `new` method, `@_attributes` represents the attributes you defined in `FooResource` class using `attributes` DSL.\n\nOther things are not so important, but you need to take care of corresponding part when you use additional features such as `root_key`, `transform_keys` and adding params.\n\n2. Logging\n\nAlba currently doesn't support logging directly, but you can add your own logging module to Alba easily.\n\n```ruby\nmodule Logging\n  # `...` was added in Ruby 2.7\n  def serialize(...)\n    puts serializable_hash\n    super\n  end\nend\n\nFooResource.prepend(Logging)\nFooResource.new(foo).serialize\n# =\u003e \"{:id=\u003e1}\" is printed\n```\n\nHere, we override `serialize` method with `prepend`. In overridden method we print the result of `serializable_hash` that gives the basic hash for serialization to `serialize` method. Using `...` allows us to override without knowing method signature of `serialize`.\n\nDon't forget calling `super` in this way.\n\n## Tips and Tricks\n\n### Treating specific classes as non-collection\n\nSometimes we need to serialize an object that's `Enumerable` but not a collection. By default, Alba treats `Hash`, `Range` and `Struct` as non-collection object, but if we want to add some classes to this list, we can override `Alba.collection?` method like following:\n\n```ruby\nAlba.singleton_class.prepend(\n  Module.new do\n    def collection?(object)\n      super \u0026\u0026 !object.is_a?(SomeClass)\n    end\n  end\n)\n```\n\n### Adding indexes to `many` association\n\nLet's say an author has many books. We want returned JSON to include indexes of each book. In this case, we can reduce the number of executed SQL by fetching indexes ahead and push indexes into `param`.\n\n```ruby\nAuthor = Data.define(:id, :books)\nBook = Data.define(:id, :name)\n\nbook1 = Book.new(1, 'book1')\nbook2 = Book.new(2, 'book2')\nbook3 = Book.new(3, 'book3')\n\nauthor = Author.new(2, [book2, book3, book1])\n\nclass AuthorResource\n  include Alba::Resource\n\n  attributes :id\n  many :books do\n    attributes :id, :name\n    attribute :index do |bar|\n      params[:index][bar.id]\n    end\n  end\nend\n\nAuthorResource.new(\n  author,\n  params: {\n    index: author.books.map.with_index { |book, index| [book.id, index] }\n    .to_h\n  }\n).serialize\n# =\u003e {\"id\":2,\"books\":[{\"id\":2,\"name\":\"book2\",\"index\":0},{\"id\":3,\"name\":\"book3\",\"index\":1},{\"id\":1,\"name\":\"book1\",\"index\":2}]}\n```\n\n## Rails\n\nWhen you use Alba in Rails, you can create an initializer file with the line below for compatibility with Rails JSON encoder.\n\n```ruby\nAlba.backend = :active_support\n# or\nAlba.backend = :oj_rails\n```\n\nTo find out more details, please see https://github.com/okuramasafumi/alba/blob/main/docs/rails.md\n\n## Why named \"Alba\"?\n\nThe name \"Alba\" comes from \"albatross\", a kind of birds. In Japanese, this bird is called \"Aho-dori\", which means \"stupid bird\". I find it funny because in fact albatrosses fly really fast. I hope Alba looks stupid but in fact it does its job quick.\n\n## Pioneers\n\nThere are great pioneers in Ruby's ecosystem which does basically the same thing as Alba does. To name a few:\n\n* [ActiveModelSerializers](https://github.com/rails-api/active_model_serializers) a.k.a AMS, the most famous implementation of JSON serializer for Ruby\n* [Blueprinter](https://github.com/procore/blueprinter) shares some concepts with Alba\n\n## Development\n\nAfter checking out the repo, run `bin/setup` to install dependencies. Then, run `bundle exec rake test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.\n\nTo 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 tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).\n\n## Contributing\n\nThank you for begin interested in contributing to Alba! Please see [contributors guide](https://github.com/okuramasafumi/alba/blob/main/CONTRIBUTING.md) before start contributing. If you have any questions, please feel free to ask in [Discussions](https://github.com/okuramasafumi/alba/discussions).\n\n## Versioning\n\nAlba follows [Semver 2.0.0](https://semver.org/spec/v2.0.0.html).\n\n## License\n\nThe gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).\n\n## Code of Conduct\n\nEveryone interacting in the Alba project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/okuramasafumi/alba/blob/main/CODE_OF_CONDUCT.md).\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fokuramasafumi%2Falba","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fokuramasafumi%2Falba","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fokuramasafumi%2Falba/lists"}