Blueprinter is a JSON Object Presenter for Ruby that takes business objects and breaks them down into simple hashes and serializes them to JSON. It can be used in Rails in place of other serializers (like JBuilder or ActiveModelSerializers). It is designed to be simple, direct, and performant.
It heavily relies on the idea of views
which, similar to Rails views, are ways of predefining output for data in different contexts.
Docs can be found here.
If you have an object you would like serialized, simply create a blueprint. Say, for example, you have a User record with the following attributes [:uuid, :email, :first_name, :last_name, :password, :address]
.
You may define a simple blueprint like so:
class UserBlueprint < Blueprinter::Base
identifier :uuid
fields :first_name, :last_name, :email
end
and then, in your code:
puts UserBlueprint.render(user) # Output is a JSON string
And the output would look like:
{
"uuid": "733f0758-8f21-4719-875f-262c3ec743af",
"email": "[email protected]",
"first_name": "John",
"last_name": "Doe"
}
You can also pass a collection object or an array to the render method.
puts UserBlueprint.render(User.all)
This will result in JSON that looks something like this:
[
{
"uuid": "733f0758-8f21-4719-875f-262c3ec743af",
"email": "[email protected]",
"first_name": "John",
"last_name": "Doe"
},
{
"uuid": "733f0758-8f21-4719-875f-743af262c3ec",
"email": "[email protected]",
"first_name": "John",
"last_name": "Doe 2"
}
]
You can rename the resulting JSON keys in both fields and associations by using the name
option.
class UserBlueprint < Blueprinter::Base
identifier :uuid
field :email, name: :login
association :user_projects, name: :projects
end
This will result in JSON that looks something like this:
{
"uuid": "92a5c732-2874-41e4-98fc-4123cd6cfa86",
"login": "[email protected]",
"projects": []
}
You may define different outputs by utilizing views:
class UserBlueprint < Blueprinter::Base
identifier :uuid
field :email, name: :login
view :normal do
fields :first_name, :last_name
end
view :extended do
include_view :normal
field :address
association :projects
end
end
Usage:
puts UserBlueprint.render(user, view: :extended)
Output:
{
"uuid": "733f0758-8f21-4719-875f-262c3ec743af",
"address": "123 Fake St.",
"first_name": "John",
"last_name": "Doe",
"login": "[email protected]"
}
You can also optionally pass in a root key to wrap your resulting json in:
class UserBlueprint < Blueprinter::Base
identifier :uuid
field :email, name: :login
view :normal do
fields :first_name, :last_name
end
end
Usage:
puts UserBlueprint.render(user, view: :normal, root: :user)
Output:
{
"user": {
"uuid": "733f0758-8f21-4719-875f-262c3ec743af",
"first_name": "John",
"last_name": "Doe",
"login": "[email protected]"
}
}
You can additionally add meta-data to the json as well:
class UserBlueprint < Blueprinter::Base
identifier :uuid
field :email, name: :login
view :normal do
fields :first_name, :last_name
end
end
Usage:
json = UserBlueprint.render(user, view: :normal, root: :user, meta: {links: [
'https://app.mydomain.com',
'https://alternate.mydomain.com'
]})
puts json
Output:
{
"user": {
"uuid": "733f0758-8f21-4719-875f-262c3ec743af",
"first_name": "John",
"last_name": "Doe",
"login": "[email protected]"
},
"meta": {
"links": [
"https://app.mydomain.com",
"https://alternate.mydomain.com"
]
}
}
Note: For meta attributes, a root is mandatory.
You can specifically choose to exclude certain fields for specific views
class UserBlueprint < Blueprinter::Base
identifier :uuid
field :email, name: :login
view :normal do
fields :first_name, :last_name
end
view :extended do
include_view :normal
field :address
exclude :last_name
end
end
Usage:
puts UserBlueprint.render(user, view: :extended)
Output:
{
"uuid": "733f0758-8f21-4719-875f-262c3ec743af",
"address": "123 Fake St.",
"first_name": "John",
"login": "[email protected]"
}
You may include associated objects. Say for example, a user has projects:
class ProjectBlueprint < Blueprinter::Base
identifier :uuid
field :name
end
class UserBlueprint < Blueprinter::Base
identifier :uuid
field :email, name: :login
view :normal do
fields :first_name, :last_name
association :projects, blueprint: ProjectBlueprint
end
end
Usage:
puts UserBlueprint.render(user, view: :normal)
Output:
{
"uuid": "733f0758-8f21-4719-875f-262c3ec743af",
"first_name": "John",
"last_name": "Doe",
"login": "[email protected]",
"projects": [
{
"uuid": "dca94051-4195-42bc-a9aa-eb99f7723c82",
"name": "Beach Cleanup"
},
{
"uuid": "eb881bb5-9a51-4d27-8a29-b264c30e6160",
"name": "Storefront Revamp"
}
]
}
By default, an association or field that evaluates to nil
is serialized as nil
. A default serialized value can be specified as an option on the association or field for cases when the association/field could potentially evaluate to nil
. You can also specify a global field_default
or association_default
in the Blueprinter config which will be used for all fields/associations that evaluate to nil.
Blueprinter.configure do |config|
config.field_default = "N/A"
config.association_default = {}
end
class UserBlueprint < Blueprinter::Base
identifier :uuid
view :normal do
field :first_name, default: "N/A"
association :company, blueprint: CompanyBlueprint, default: {}
end
end
When defining an association, we can dynamically evaluate the blueprint. This comes in handy when adding polymorphic associations, by allowing reuse of existing blueprints.
class Task < ActiveRecord::Base
belongs_to :taskable, polymorphic: true
end
class Project < ActiveRecord::Base
has_many :tasks, as: :taskable
def blueprint
ProjectBlueprint
end
end
class TaskBlueprint < Blueprinter::Base
identifier :uuid
view :normal do
field :title, default: "N/A"
association :taskable, blueprint: ->(taskable) {taskable.blueprint}, default: {}
end
end
Note: taskable.blueprint
should return a valid Blueprint class. Currently, has_many
is not supported because of the very nature of polymorphic associations.
You can define a field directly in the Blueprint by passing it a block. This is especially useful if the object does not already have such an attribute or method defined, and you want to define it specifically for use with the Blueprint. This is done by passing field
a block. The block also yields the object and any options that were passed from render
. For example:
class UserBlueprint < Blueprinter::Base
identifier :uuid
field :full_name do |user, options|
"#{options[:title_prefix]} #{user.first_name} #{user.last_name}"
end
end
Usage:
puts UserBlueprint.render(user, title_prefix: "Mr")
Output:
{
"uuid": "733f0758-8f21-4719-875f-262c3ec743af",
"full_name": "Mr John Doe"
}
You can also pass a block to an identifier:
class UserBlueprint < Blueprinter::Base
identifier :uuid do |user, options|
options[:current_user].anonymize(user.uuid)
end
end
Usage:
puts UserBlueprint.render(user, current_user: current_user)
Output:
{
"uuid": "733f0758-8f21-4719-875f-262c3ec743af",
}
You can also pass a block to an association:
class ProjectBlueprint < Blueprinter::Base
identifier :uuid
field :name
end
class UserBlueprint < Blueprinter::Base
identifier :uuid
association :projects, blueprint: ProjectBlueprint do |user, options|
user.projects + options[:draft_projects]
end
end
Usage:
puts UserBlueprint.render(user, draft_projects: Project.where(draft: true))
Output:
{
"uuid": "733f0758-8f21-4719-875f-262c3ec743af",
"projects": [
{"uuid": "b426a1e6-ac41-45ab-bfef-970b9a0b4289", "name": "query-console"},
{"uuid": "5bd84d6c-4fd2-4e36-ae31-c137e39be542", "name": "blueprinter"},
{"uuid": "785f5cd4-7d8d-4779-a6dd-ec5eab440eff", "name": "uncontrollable"}
]
}
render
takes an options hash which you can pass additional properties, allowing you to utilize those additional properties in the field
block. For example:
class UserBlueprint < Blueprinter::Base
identifier :uuid
field(:company_name) do |_user, options|
options[:company].name
end
end
Usage:
puts UserBlueprint.render(user, company: company)
Output:
{
"uuid": "733f0758-8f21-4719-875f-262c3ec743af",
"company_name": "My Company LLC"
}
Same as render
, returns a Ruby Hash.
Usage:
puts UserBlueprint.render_as_hash(user, company: company)
Output:
{
uuid: "733f0758-8f21-4719-875f-262c3ec743af",
company_name: "My Company LLC"
}
Same as render
, returns a Ruby Hash JSONified. This will call JSONify all keys and values.
Usage:
puts UserBlueprint.render_as_json(user, company: company)
Output:
{
"uuid" => "733f0758-8f21-4719-875f-262c3ec743af",
"company_name" => "My Company LLC"
}
Both the field
and the global Blueprinter Configuration supports :if
and :unless
options that can be used to serialize fields conditionally.
Blueprinter.configure do |config|
config.if = ->(obj, _options) { obj.is_a?(Foo) }
config.unless = ->(obj, _options) { obj.is_a?(Bar) }
end
class UserBlueprint < Blueprinter::Base
identifier :uuid
field :last_name, if: ->(user, options) { user.first_name != options[:first_name] }
field :age, unless: ->(user, _options) { user.age < 18 }
end
The field-level setting overrides the global config setting (for the field) if both are set.
To define a custom format for a Date or DateTime field, include the option datetime_format
with the associated strptime
format.
Usage:
class UserBlueprint < Blueprinter::Base
identifier :name
field :birthday, datetime_format: "%m/%d/%Y"
end
Output:
{
"name": "John Doe",
"birthday": "03/04/1994"
}
Add this line to your application's Gemfile:
gem 'blueprinter'
And then execute:
$ bundle
Or install it yourself as:
$ gem install blueprinter
You should also have require 'json'
already in your project if you are not using Rails or if you are not using Oj.
By default the response sorts the keys by name. If you want the fields to be sorted in the order of definition, use the below configuration option.
Usage:
Blueprinter.configure do |config|
config.sort_fields_by = :definition
end
class UserBlueprint < Blueprinter::Base
identifier :name
field :email
field :birthday, datetime_format: "%m/%d/%Y"
end
Output:
{
"name": "John Doe",
"email": "[email protected]",
"birthday": "03/04/1994"
}
By default, Blueprinter will be calling JSON.generate(object)
internally and it expects that you have require 'json'
already in your project's code. You may use Oj
to generate in place of JSON
like so:
require 'oj' # you can skip this if OJ has already been required.
Blueprinter.configure do |config|
config.generator = Oj # default is JSON
end
Ensure that you have the Oj
gem installed in your Gemfile if you haven't already:
# Gemfile
gem 'oj'
yajl-ruby is a fast and powerful JSON generator/parser. To use yajl-ruby
in place of JSON / OJ
, use:
require 'yajl' # you can skip this if yajl has already been required.
Blueprinter.configure do |config|
config.generator = Yajl::Encoder # default is JSON
config.method = :encode # default is generate
end
Note: You should be doing this only if you aren't using yajl-ruby
through the JSON API by requiring yajl/json_gem
. More details here. In this case, JSON.generate
is patched to use Yajl::Encoder.encode
internally.
We use Yard for documentation. Here are the following documentation rules:
- Document all public methods we expect to be utilized by the end developers.
- Methods that are not set to private due to ruby visibility rule limitations should be marked with
@api private
.
Feel free to browse the issues, converse, and make pull requests. If you need help, first please see if there is already an issue for your problem. Otherwise, go ahead and make a new issue.
You can run tests with bundle exec rake
.
We use Yard for documentation. Here are the following documentation rules:
- Document all public methods we expect to be utilized by the end developers.
- Methods that are not set to private due to ruby visibility rule limitations should be marked with
@api private
.
To release a new version, change the version number in version.rb
, and update the CHANGELOG.md
. Finally, maintainers need to run bundle exec rake release
, which will automatically create a git tag for the version, push git commits and tags to Github, and push the .gem
file to rubygems.org.
The gem is available as open source under the terms of the MIT License.