Please change your local remote to pull from this repository:
git remote set-url [previous-remote-name] git@github.com:procore-oss/blueprinter.gitto see the previous upstream remote name, run:
git remote -vBlueprinter 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.
Basic
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:
classUserBlueprint < Blueprinter::Baseidentifier:uuidfields:first_name,:last_name,:emailendand then, in your code:
putsUserBlueprint.render(user)# Output is a JSON stringAnd the output would look like:
{
"uuid": "733f0758-8f21-4719-875f-262c3ec743af",
"email": "john.doe@some.fake.email.domain",
"first_name": "John",
"last_name": "Doe"
}Collections
You can also pass a collection object or an array to the render method.
putsUserBlueprint.render(User.all)This will result in JSON that looks something like this:
[
{
"uuid": "733f0758-8f21-4719-875f-262c3ec743af",
"email": "john.doe@some.fake.email.domain",
"first_name": "John",
"last_name": "Doe"
},
{
"uuid": "733f0758-8f21-4719-875f-743af262c3ec",
"email": "john.doe.2@some.fake.email.domain",
"first_name": "John",
"last_name": "Doe 2"
}
]You can also configure other classes to be treated like collections. For example, if you are using Mongoid, you can configure it to treat Mongoid::Criteria objects as collections:
Blueprinter.configuredo |config|
config.custom_array_like_classes=[Mongoid::Criteria]endOr if you wanted it to treat the Set class as a collection:
Blueprinter.configuredo |config|
config.custom_array_like_classes=[Set]endRenaming
You can rename the resulting JSON keys in both fields and associations by using the name option.
classUserBlueprint < Blueprinter::Baseidentifier:uuidfield:email,name: :loginassociation:user_projects,name: :projectsendThis will result in JSON that looks something like this:
{
"uuid": "92a5c732-2874-41e4-98fc-4123cd6cfa86",
"login": "my@email.com",
"projects": []
}Views
You may define different outputs by utilizing views:
classUserBlueprint < Blueprinter::Baseidentifier:uuidfield:email,name: :loginview:normaldofields:first_name,:last_nameendview:extendeddoinclude_view:normalfield:addressassociation:projectsendendA view can include fields from another view by utilizing include_view and include_views.
Usage:
putsUserBlueprint.render(user,view: :extended)Output:
{
"uuid": "733f0758-8f21-4719-875f-262c3ec743af",
"address": "123 Fake St.",
"first_name": "John",
"last_name": "Doe",
"login": "john.doe@some.fake.email.domain"
}Identifiers
identifiers are used to specify a field or method name used as an identifier. Usually, this is something like :id.
Example:
classUserBlueprint < Blueprinter::Baseidentifier:uuidendBlueprinter identifiers have a few properties that set them apart from fields.
- Identifiers are always rendered and considered their own view (the
:identifierview). - When rendering, identifier fields are always sorted first, before other fields.
If either of the above two developer conveniences are not desired, you can simply create your identifier fields as regular fields.
Root
You can also optionally pass in a root key to wrap your resulting json in:
classUserBlueprint < Blueprinter::Baseidentifier:uuidfield:email,name: :loginview:normaldofields:first_name,:last_nameendendUsage:
putsUserBlueprint.render(user,view: :normal,root: :user)Output:
{
"user": {
"uuid": "733f0758-8f21-4719-875f-262c3ec743af",
"first_name": "John",
"last_name": "Doe",
"login": "john.doe@some.fake.email.domain"
}
}Meta Attributes
You can additionally add meta-data to the json as well:
classUserBlueprint < Blueprinter::Baseidentifier:uuidfield:email,name: :loginview:normaldofields:first_name,:last_nameendendUsage:
json=UserBlueprint.render(user,view: :normal,root: :user,meta: {links: ['https://app.mydomain.com','https://alternate.mydomain.com']})putsjsonOutput:
{
"user": {
"uuid": "733f0758-8f21-4719-875f-262c3ec743af",
"first_name": "John",
"last_name": "Doe",
"login": "john.doe@some.fake.email.domain"
},
"meta": {
"links": [
"https://app.mydomain.com",
"https://alternate.mydomain.com"
]
}
}NOTE: For meta attributes, a root is mandatory.
Exclude Fields
You can specifically choose to exclude certain fields for specific views
classUserBlueprint < Blueprinter::Baseidentifier:uuidfield:email,name: :loginview:normaldofields:first_name,:last_nameendview:extendeddoinclude_view:normalfield:addressexclude:last_nameendendUsage:
putsUserBlueprint.render(user,view: :extended)Output:
{
"uuid": "733f0758-8f21-4719-875f-262c3ec743af",
"address": "123 Fake St.",
"first_name": "John",
"login": "john.doe@some.fake.email.domain"
}Use excludes to exclude multiple fields at once inline.
classUserBlueprint < Blueprinter::Baseidentifier:uuidfield:email,name: :loginview:normaldofields:age,:first_name,:last_name,endview:extendeddoinclude_view:normalfield:addressexcludes:age,:last_nameendendAssociations
You may include associated objects. Say for example, a user has projects:
classProjectBlueprint < Blueprinter::Baseidentifier:uuidfield:nameendclassUserBlueprint < Blueprinter::Baseidentifier:uuidfield:email,name: :loginview:normaldofields:first_name,:last_nameassociation:projects,blueprint: ProjectBlueprintendendUsage:
putsUserBlueprint.render(user,view: :normal)Output:
{
"uuid": "733f0758-8f21-4719-875f-262c3ec743af",
"first_name": "John",
"last_name": "Doe",
"login": "john.doe@some.fake.email.domain",
"projects": [
{
"uuid": "dca94051-4195-42bc-a9aa-eb99f7723c82",
"name": "Beach Cleanup"
},
{
"uuid": "eb881bb5-9a51-4d27-8a29-b264c30e6160",
"name": "Storefront Revamp"
}
]
}It is also possible to pass options from one Blueprint to another via an association. For example:
classVehicleBlueprint < Blueprinter::Baseidentifier:uuidfield:full_namedo |vehicle,options|
"#{vehicle.model}#{options[:trim]}"endendclassDriverBlueprint < Blueprinter::Baseidentifier:uuidview:normaldofields:first_name,:last_nameassociation:vehicles,blueprint: VehicleBlueprint,options: {trim: 'LX'}endendThe options parameter also accepts a Proc (or any callable) for cases where the options need to be derived from the parent object at render time. The callable is invoked with the parent object and must return a Hash, which is then merged into the options passed to the associated Blueprint.
classDriverBlueprint < Blueprinter::Baseidentifier:uuidview:normaldofields:first_name,:last_nameassociation:vehicles,blueprint: VehicleBlueprint,options: ->(driver){{trim: driver.preferred_trim}}endendDefault Association/Field Option
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.configuredo |config|
config.field_default="N/A"config.association_default={}endclassUserBlueprint < Blueprinter::Baseidentifier:uuidview:normaldofield:first_name,default: "N/A"association:company,blueprint: CompanyBlueprint,default: {}endenddefault_if
Sometimes, you may want certain "empty" values to pass through to the default value.
Blueprinter provides the ability to treat the following empty types as the default value (or nil if no default provided).
An empty array or empty active record collection.
An empty hash.
An empty string or symbol.
classUserBlueprint < Blueprinter::Baseidentifier:uuidview:normaldo# If first_name is an empty string, it will become "N/A"field:first_name,default_if: Blueprinter::EMPTY_STRING,default: "N/A"# If the projects association collection is empty, it will become nilassociation:projects,blueprint: ProjectBlueprint,default_if: Blueprinter::EMPTY_COLLECTIONendendSupporting Dynamic Blueprints For Associations
When defining an association, we can dynamically evaluate the blueprint. This comes in handy when adding polymorphic associations, by allowing reuse of existing blueprints.
classTask < ActiveRecord::Basebelongs_to:taskable,polymorphic: trueendclassProject < ActiveRecord::Basehas_many:tasks,as: :taskabledefblueprintProjectBlueprintendendclassTaskBlueprint < Blueprinter::Baseidentifier:uuidview:normaldofield:title,default: "N/A"association:taskable,blueprint: ->(taskable){taskable.blueprint},default: {}endendNOTE:taskable.blueprint should return a valid Blueprint class. Currently, has_many is not supported because of the very nature of polymorphic associations.
Defining A Field Directly In The Blueprint
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:
classUserBlueprint < Blueprinter::Baseidentifier:uuidfield:full_namedo |user,options|
"#{options[:title_prefix]}#{user.first_name}#{user.last_name}"endendUsage:
putsUserBlueprint.render(user,title_prefix: "Mr")Output:
{
"uuid": "733f0758-8f21-4719-875f-262c3ec743af",
"full_name": "Mr John Doe"
}When a field block just delegates to a single method on the object, Symbol#to_proc (&:method_name) can be used as a terser form of the block syntax:
classUserBlueprint < Blueprinter::Baseidentifier:uuidfield:display_name, &:formatted_full_nameendThis is equivalent to:
field:display_namedo |user|
user.formatted_full_nameendNOTE: When using Symbol#to_proc, the block is invoked with only the object — options are not available, and the configured extractor is bypassed. For simple renames of an existing attribute, prefer the name: option instead (see Renaming), which routes through the configured extractor and keeps the DSL consistent.
Accessing the View Option
The view provided to the render call is implicitly available in the options hash within a field block, and can be referenced as needed. For example:
classUserBlueprint < Blueprinter::Baseidentifier:uuidfield:full_namedo |user,options|
prefix=options[:view] == :admin ? '[Admin]' : options[:title_prefix]"#{prefix}#{user.first_name}#{user.last_name}"endview:admindofield:access_levelendendUsage:
putsUserBlueprint.render(user,title_prefix: "Mr",view: :admin)Output:
{
"uuid": "733f0758-8f21-4719-875f-262c3ec743af",
"full_name": "[Admin] John Doe",
"access_level": "4"
}Defining An Identifier Directly In The Blueprint
You can also pass a block to an identifier:
classUserBlueprint < Blueprinter::Baseidentifier:uuiddo |user,options|
options[:current_user].anonymize(user.uuid)endendUsage:
putsUserBlueprint.render(user,current_user: current_user)Output:
{
"uuid": "733f0758-8f21-4719-875f-262c3ec743af",
}Identifiers also accept Symbol#to_proc shorthand as a terser form of the block syntax when the value is a single method call on the object:
classUserBlueprint < Blueprinter::Baseidentifier:anonymized_id, &:obfuscated_uuidendNOTE: As with fields, this bypasses the configured extractor and does not have access to options. To use a different key name than the underlying method (without a block), use identifier :method_name, name: :key_name.
Defining An Association Directly In The Blueprint
You can also pass a block to an association:
classProjectBlueprint < Blueprinter::Baseidentifier:uuidfield:nameendclassUserBlueprint < Blueprinter::Baseidentifier:uuidassociation:projects,blueprint: ProjectBlueprintdo |user,options|
user.projects + options[:draft_projects]endendUsage:
putsUserBlueprint.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"}
]
}Passing Additional Properties To #render
render takes an options hash which you can pass additional properties, allowing you to utilize those additional properties in the field block. For example:
classUserBlueprint < Blueprinter::Baseidentifier:uuidfield(:company_name)do |_user,options|
options[:company].nameendendUsage:
putsUserBlueprint.render(user,company: company)Output:
{
"uuid": "733f0758-8f21-4719-875f-262c3ec743af",
"company_name": "My Company LLC"
}Conditional Fields
Both the field and the global Blueprinter Configuration supports :if and :unless options that can be used to serialize fields conditionally.
Blueprinter.configuredo |config|
config.if=->(field_name,obj,_options){ !obj[field_name].nil?}config.unless=->(field_name,obj,_options){obj[field_name].nil?}endclassUserBlueprint < Blueprinter::Baseidentifier:uuidfield:last_name,if: ->(_field_name,user,options){user.first_name != options[:first_name]}field:age,unless: ->(_field_name,user,_options){user.age < 18}endNOTE: The field-level setting overrides the global config setting (for the field) if both are set.
Exclude Fields with nil Values
By default, fields with nil values are included when rendering. You can override this behavior by setting :exclude_if_nil: true in the field definition.
Usage:
classUserBlueprint < Blueprinter::Baseidentifier:uuidfield:namefield:birthday,exclude_if_nil: trueenduser=User.new(name: 'John Doe')putsUserBlueprint.render(user)Output:
{
"name": "John Doe"
}Custom Formatting for Dates and Times
To define a custom format for a Date or DateTime field, include the option datetime_format.
This global or field-level option can be either a string representing the associated strftime format,
or a Proc which receives the original Date/DateTime object and returns the formatted value.
When using a Proc, it is the Proc's responsibility to handle any errors in formatting.
If a global datetime_format is set (either as a string format or a Proc), this option will be
invoked and used to format all fields that respond to strftime.
Blueprinter.configuredo |config|
config.datetime_format=->(datetime){datetime.nil? ? datetime : datetime.strftime("%s").to_i}endUsage (String Option):
classUserBlueprint < Blueprinter::Baseidentifier:namefield:birthday,datetime_format: "%m/%d/%Y"endOutput:
{
"name": "John Doe",
"birthday": "03/04/1994"
}Usage (Proc Option):
classUserBlueprint < Blueprinter::Baseidentifier:namefield:birthday,datetime_format: ->(datetime){datetime.nil? ? datetime : datetime.strftime("%s").to_i}endOutput:
{
"name": "John Doe",
"birthday": 762739200
}NOTE: The field-level setting overrides the global config setting (for the field) if both are set.
Transform Classes
Blueprinter provides the ability to specify transforms on views, which enable further
processing and transforming of resulting view field hashes prior to serialization.
Use transform to specify one transformer to be included for serialization.
A transformer is a class, extending Blueprinter::Transformer and implementing the transform method.
The modified hash object will be the resulting hash passed to serialization.
Create a Transform class extending from Blueprinter::Transformer
classDynamicFieldTransformer < Blueprinter::Transformerdeftransform(hash,object,_options)hash.merge!(object.dynamic_fields)endendclassUserdefdynamic_fieldscaserolewhen:admin{employer: employer,time_in_role: determine_time_inrole}when:maintainer{label: label,settings: generate_settings_hash}when:read_only{last_login_at: last_login_at}endendendThen specify the transform to use for the view.
classUserBlueprint < Blueprinter::Basefields:first_name,:last_nametransformDynamicTransformerendTransformers can be included across views:
classUserBlueprint < Blueprinter::BasetransformDefaultTransformerview:normaldotransformViewTransformerendview:extendeddoinclude_view:normalendendBoth the normal and extended views have DefaultTransformer and ViewTransformer applied.
Transformers are executed in a top-down order, so DefaultTransformer will be executed first, followed by ViewTransformer.
You can also specify global default transformers. Create one or more transformer classes extending from Blueprinter::Transformer and set the default_transformers configuration
classLowerCamelTransformer < Blueprinter::Transformerdeftransform(hash,_object,_options)hash.transform_keys!{ |key| key.to_s.camelize(:lower).to_sym}endendBlueprinter.configuredo |config|
config.default_transformers=[LowerCamelTransformer]endNote: Any transforms specified on a per-blueprint or per-view level will override the default_transformers in the configuration.
Configurable Extractors
Blueprinter gets a given objects' values from the fields definitions using extractor classes. You can substitute your own extractor class globally or per-field.
For a specific kind of field, create an extractor class extending from Blueprinter::Extractor
classMyFieldExtractor < Blueprinter::Extractordefextract(_field_name,_object,_local_options,_options={})# process your obscure_object_object.clarifiedendendclassMysteryBlueprint < Blueprinter::Basefield:obscure_object,extractor: MyFieldExtractorendFor a global default, create an extractor class extending from Blueprinter::AutoExtractor and set the extractor_default configuration
classMyAutoExtractor < Blueprinter::AutoExtractordefinitializesuper@my_field_extractor=MyFieldExtractor.newenddefextractor(object,options)# dispatch to any class AutoExtractor can, plus moreifdetect_obscurity(object)@my_field_extractorelsesuperendendendBlueprinter.configuredo |config|
config.extractor_default=MyAutoExtractorendSorting Fields
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.configuredo |config|
config.sort_fields_by=:definitionendclassUserBlueprint < Blueprinter::Baseidentifier:namefield:emailfield:birthday,datetime_format: "%m/%d/%Y"endOutput:
{
"name": "John Doe",
"email": "john.doe@some.fake.email.domain",
"birthday": "03/04/1994"
}Reflection
Blueprint classes may be reflected on to inspect their views, fields, and associations. Extensions often make use of this ability.
classWidgetBlueprint < Blueprinter::Basefields:name,:descriptionassociation:category,blueprint: CategoryBlueprintview:extendeddofield:priceassociation:parts,blueprint: WidgetPartBlueprintendend# A Hash of views keyed by nameviews=WidgetBlueprint.reflectionsviews.keys=>[:default,:extended]# Hashes of fields and associations, keyed by namefields=views[:default].fieldsassoc=views[:default].associations# Get info about a fieldfields[:description].namefields[:description].display_namefields[:description].options# Get info about an associationassoc[:category].nameassoc[:category].display_nameassoc[:category].blueprintassoc[:category].viewassoc[:category].optionsExtensions
Blueprinter provides an extension system that enables certain behavior to be modified as needed.
Blueprinter.configuredo |config|
config.extensions << MyExtension.newconfig.extensions << OtherExtension.newend- pre_render - Intercept the object before rendering begins. This allows you to modify, transform, or replace the object that will be serialized.
To create an extension, simply subclass Blueprinter::Extension and override the method representing the desired hook:
classObfuscateNameExtension < Blueprinter::Extensiondefpre_render(object,blueprint,view,options)returnobjectunlessobject.respond_to?(:name)modified_object=object.dupmodified_object.name=ObsfuscateName.call(modified_object.name)modified_objectendendExtensions are executed in the order they are added to the configuration. Each extension receives the result from the previous extension, allowing for chained transformations:
Blueprinter.configuredo |config|
config.extensions << SecurityExtension.new# Runs firstconfig.extensions << AssociationLoaderExtension.new# Runs secondconfig.extensions << UserEnrichmentExtension.new# Runs thirdendGems can be created that enrich blueprinter's core functionality via extensions.
- blueprinter-activerecord - Provides ActiveRecord-specific optimizations and features
NOTE: The following are not officially maintained/supported by Procore OSS.
- blueprinter_schema - Create JSON Schemas from Blueprinter Serializers
Deprecations
When functionality in Blueprinter is invoked, that has been deprecated, the default behavior is to write a deprecation notice to stderror.
However, deprecations can be configured to report at three different levels:
| Key | Result |
|---|---|
:stderr (Default) | Deprecations will be written to stderror |
:raise | Deprecations will be raised as Blueprinter::BlueprinterErrors |
:silence | Deprecations will be silenced and will not be raised or logged |
Blueprinter.configuredo |config|
config.deprecations=:raiseendrender_as_hash
Same as render, returns a Ruby Hash.
Usage:
putsUserBlueprint.render_as_hash(user,company: company)Output:
{uuid: "733f0758-8f21-4719-875f-262c3ec743af",company_name: "My Company LLC"}render_as_json
Same as render, returns a Ruby Hash JSONified. This will call JSONify all keys and values.
Usage:
putsUserBlueprint.render_as_json(user,company: company)Output:
{"uuid"=>"733f0758-8f21-4719-875f-262c3ec743af","company_name"=>"My Company LLC"}Add this line to your application's Gemfile:
gem'blueprinter'And then execute:
bundleOr install it yourself as:
gem install blueprinterYou should also have require 'json' already in your project if you are not using Rails or if you are not using Oj.
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.configuredo |config|
config.generator=Oj# default is JSONendEnsure that you have the Oj gem installed in your Gemfile if you haven't already:
# Gemfilegem'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.configuredo |config|
config.generator=Yajl::Encoder# default is JSONconfig.method=:encode# default is generateendNOTE: 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.
Please read our Contributing file
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.
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.