Blueprinter
NOTE This is a WIP for API V2!
Blueprinter is a JSON serializer for your business objects. It is designed to be simple, flexible, and performant.
Upgrading from 1.x? Read the upgrade guide!
Installation
bundle add blueprinter
See rubydoc.info/gems/blueprinter for generated API documentation.
Basic Usage
class WidgetBlueprint < ApplicationBlueprint
field :name
object :category, CategoryBlueprint
collection :parts, PartBlueprint
view :extended do
field :description
object :manufacturer, CompanyBlueprint
collection :vendors, CompanyBlueprint
end
end
# Render the default view to JSON
WidgetBlueprint.render(widget).to_json
# Render the extended view to a Hash
WidgetBlueprint[:extended].render(widget).to_hash
Look interesting? Learn the DSL!
Blueprinter DSL
Define your base class
Define an ApplicationBlueprint for your blueprints to inherit from. Any global configuration goes here: common fields, views, partials, formatters, extensions, and options.
class ApplicationBlueprint < Blueprinter::Blueprint
extensions << MyExtension.new
options[:exclude_if_nil] = true
field :id
end
Define blueprints for your models
This blueprint inherits everything from ApplicationBlueprint, then adds a name field and two associations that will render using other blueprints.
class WidgetBlueprint < ApplicationBlueprint
field :name
object :category, CategoryBlueprint
collection :parts, PartBlueprint
end
There's a lot more you can do with the Blueprinter DSL. Fields are a good place to start!
Fields
# Use field for scalar values, arrays of scalar values, or even a Hash
field :name
field :tags
# Add multiple fields at once
fields :description, :price
# Use object to render an object or Hash using another blueprint
object :category, CategoryBlueprint
# Use collection to render an array-like collection of objects
collection :parts, PartBlueprint
Options
Fields accept a wide array of built-in options, and extensions can define even more. Find all built-in options here.
field :description, default: "No description"
collection :parts, PartBlueprint, exclude_if_empty: true
Extracting field values
Blueprinter is pretty smart about extracting field values from objects, but there are ways to customize the behavior if needed.
Default behavior
- For Hashes, Blueprinter will look for a key matching the field name - first with a Symbol, then a String.
- For anything else, Blueprinter will look for a public method matching the field name.
- The from field option can be used to specify a different method or Hash key name.
Field blocks
Return whatever you want from a block. It will be passed a Field context argument containing the object being rendered, among other things.
field :description do |ctx|
ctx.object.description.upcase
end
# Blocks can call instance methods defined on your Blueprint
collection :parts, PartBlueprint do |ctx|
active_parts ctx.object
end
def active_parts(object)
object.parts.select(&:active?)
end
Custom extractors
Define your own extraction behavior with a custom extractor.
# For an entire Blueprint or view
extensions << MyCustomExtractor.new
# For a single field
object :bar, extractor: MyCustomExtractor
Views
Blueprints can define views to provide different representations of the data. A view inherits everything from its parent but is free to override as needed. In addition to fields, views can define options, partials, formatters, extensions, and nested views.
class WidgetBlueprint < ApplicationBlueprint
field :name
object :category, CategoryBlueprint
# The "with_parts" view inherits from "default" and adds a collection of parts
view :with_parts do
collection :parts, PartBlueprint
end
# Views can include other views
view :full do
use :with_parts
field :description
end
end
At the top level of every Blueprint is an implicit view called default. The default view is used when no other is specified. All other views in the Blueprint inherit from it.
Nesting views
You can nest views within views, allowing for a hierarchy of inheritance.
class WidgetBlueprint < ApplicationBlueprint
field :name
object :category, CategoryBlueprint
view :extended do
field :description
collection :parts, PartBlueprint
# The "extended.with_price" view adds a price field
view :with_price do
field :price
end
end
Excluding fields
Views can exclude select fields from parents, views they've included, or from partials.
class WidgetBlueprint < ApplicationBlueprint
fields :name, :description, :price
view :minimal do
exclude :description, :price
end
end
You can exclude and and all parent fields by creating an empty view:
class WidgetBlueprint < ApplicationBlueprint
fields :name, :description, :price
view :minimal, empty: true do
field :the_only_field
end
end
Referencing views
When defining an association, you can choose a view from its blueprint:
object :widget, WidgetBlueprint[:extended]
Nested views can be accessed with a dot syntax or a nested Hash syntax.
collection :widgets, WidgetBlueprint["extended.with_price"]
collection :widgets, WidgetBlueprint[:extended][:with_price]
Inheriting from views
You can inherit from another blueprint, or from one of its views:
class WidgetBlueprint < ApplicationBlueprint[:with_timestamps]
# ...
end
Partials
Partials allow you to compose views from reusable components. Just like views, partials can define fields, options, views, other partials, formatters, and extensions.
class WidgetBlueprint < ApplicationBlueprint
field :name
view :foo do
use :associations
field :foo
end
view :bar do
use :associations, :description
field :bar
end
partial :associations do
object :category, CategoryBlueprint
collection :parts, PartBlueprint
end
partial :description do
field :description
end
end
There are two ways of including partials: appending with 'use' and inserting with 'use!' (see examples).
Append with 'use'
Partials are appended to your view, giving them the opportunity to override your view's fields, options, etc. Precedence (highest to lowest) is:
- Definitions in the partial
- Definitions in the view
- Definitions inherited from the blueprint/parent views
Insert with 'use!'
Partials are embedded immediately, on that line, allowing subsequent lines to override the partial. Precedence (highest to lowest) is:
- Definitions in the view after
use! - Definitions in the partial
- Definitions in the view before
use! - Definitions inherited from the blueprint/parent views
Examples of 'use' and 'use!'
partial :no_empty_fields do
options[:field_if] = :og_field_logic
# other stuff
end
# :foo appends the partial, so it overrides the view's field_if
view :foo do
use :no_empty_fields
options[:field_if] = :other_field_logic
end
# :bar inserts the partial, but the next line overrides the partial's field_if
view :bar do
use! :no_empty_fields
options[:field_if] = :other_field_logic
end
Formatters
Declaratively format field values by class. You can define formatters anywhere in your blueprints: top level, views, and partials.
class WidgetBlueprint < ApplicationBlueprint
# Strip whitespace from all strings
format(String) { |val| val.strip }
# Format all dates and times using ISO-8601
format Date, :iso8601
format Time, :iso8601
def iso8601(val)
val.iso8601
end
end
Options
Numerous options can be defined on Blueprints, views, partials, or individual fields. Some can also be passed to render.
class WidgetBlueprint < ApplicationBlueprint
# Blueprint options apply to all fields, associations, views, and partials in
# the Blueprint. They are inherited from the parent class but can be overridden.
options[:exclude_if_empty] = true
# Field options apply to individual fields or associations. They can override
# Blueprint options.
field :name, exclude_if_empty: false
# Options in views apply to all fields, associations, partials and nested views
# in the view. They inherit options from the Blueprint, or from parent views,
# and can override them.
view :foo do
options[:exclude_if_empty] = false
end
# Options in partials apply to all fields, associations, views, and partials in
# the partial. All of these are applied to the views that use the partial.
partial :bar do
options[:exclude_if_empty] = false
end
# Some options accept Procs/labmdas. These can call instance methods defined on
# your Blueprint. Or you can pass a method name as a symbol.
field :foo, if: ->(ctx) { long_complex_check? ctx }
field :bar, if: :long_complex_check?
def long_complex_check?(ctx)
# ...
end
end
# Passing a supported option to render will override what's in the blueprint
WidgetBlueprint.render(widget, exclude_if_empty: false).to_json
For easier reference, options are grouped into the following categories:
- Default values: Provide defaults for empty fields
- Conditional fields: Exclude fields based on conditions
- Field mapping: Change how field values are extracted from objects
- Metadata: Wrap or add metadata to the output
A note about context objects
Options that accept Procs, lambdas, or method names are usually passed a Field context argument. It contains the object being rendered as well as other useful information.
Default Values
These options allow you to set default values for fields and associations, and customize when they're used.
default
A default value used when the field or assocation is nil.
Available in field, object, collection
@param Field context
field :foo, default: "Foo"
field :foo, default: ->(ctx) { "Foo" }
field :foo, default: :foo
def foo(ctx) = "Foo"
field_default
Default value for any nil non-association field in its scope.
Available in blueprint, view, partial, render
@param Field context
options[:field_default] = "Foo"
options[:field_default] = ->(ctx) { "Foo" }
options[:field_default] = :foo
def foo(ctx) = "Foo"
WidgetBluerpint.render(widget, field_default: "Foo").to_json
object_default
Default value for any nil object field in its scope.
Available in blueprint, view, partial, render
@param Field context
options[:object_default] = { name: "Foo" }
options[:object_default] = ->(ctx) { { name: "Foo" } }
options[:object_default] = :foo
def foo(ctx) = { name: "Foo" }
WidgetBluerpint.render(widget, object_default: { name: "Foo" }).to_json
collection_default
Default value for any nil collection field.
Available in blueprint, view, partial, render
@param Field context
options[:collection_default] = [{ name: "Foo" }]
options[:collection_default] = ->(ctx) { [{ name: "Foo" }] }
options[:collection_default] = :foo
def foo(ctx) = [{ name: "Foo" }]
WidgetBluerpint.render(widget, collection_default: [{ name: "Foo" }]).to_json
default_if
Use the default value if the given Proc or method name returns truthy.
Available in field, object, collection
@param Field context
field :foo, default: "Foo", default_if: ->(ctx) { ctx.object.disabled? }
field :foo, default: "Foo", default_if: :disabled?
def disabled?(ctx) = ctx.object.disabled?
field_default_if
Same as default_if, but applies to any non-association field in scope.
Available in blueprint, view, partial, render
@param Field context
options[:field_default_if] = ->(ctx) { ctx.object.disabled? }
options[:field_default_if] = :disabled?
def disabled?(ctx) = ctx.object.disabled?
WidgetBluerpint.render(widget, field_default_if: :disabled?).to_json
object_default_if
Same as default_if, but applies to any object field in scope.
Available in blueprint, view, partial, render
@param Field context
options[:object_default_if] = ->(ctx) { ctx.object.disabled? }
options[:object_default_if] = :disabled?
def disabled?(ctx) = ctx.object.disabled?
WidgetBluerpint.render(widget, object_default_if: :disabled?).to_json
collection_default_if
Same as default_if, but applies to any collection field in scope.
Available in blueprint, view, partial, render
@param Field context
options[:collection_default_if] = ->(ctx) { ctx.object.disabled? }
options[:collection_default_if] = :disabled?
def disabled?(ctx) = ctx.object.disabled?
WidgetBluerpint.render(widget, collection_default_if: :disabled?).to_json
Conditional Fields
These options allow you to exclude fields from the output.
exclude_if_nil
Exclude fields if they're nil.
Available in blueprint, view, partial, field, object, collection, render
options[:exclude_if_nil] = true
field :description, exclude_if_nil: true
WidgetBluerpint.render(widget, exclude_if_nil: true).to_json
exclude_if_empty
Exclude fields if they're nil, or if they respond to empty? and it returns true.
Available in blueprint, view, partial, field, object, collection, render
options[:exclude_if_empty] = true
field :description, exclude_if_empty: true
WidgetBluerpint.render(widget, exclude_if_empty: true).to_json
if
Only include the field if the given Proc or method name returns truthy.
Available in field, object, collection
@param Field context
field :foo, if: ->(ctx) { ctx.object.enabled? }
field :foo, if: :enabled?
def enabled?(ctx) = ctx.object.enabled?
field_if
Only include non-association fields if the given Proc or method name returns truthy.
Available in blueprint, view, partial, render
@param Field context
options[:field_if] = ->(ctx) { ctx.object.enabled? }
options[:field_if] = :enabled?
def enabled?(ctx) = ctx.object.enabled?
WidgetBluerpint.render(widget, field_if: :enabled?).to_json
object_if
Only include object fields if the given Proc or method name returns truthy.
Available in blueprint, view, partial, render
@param Field context
options[:object_if] = ->(ctx) { ctx.object.enabled? }
options[:object_if] = :enabled?
def enabled?(ctx) = ctx.object.enabled?
WidgetBluerpint.render(widget, object_if: :enabled?).to_json
collection_if
Only include collection fields if the given Proc or method name returns truthy.
Available in blueprint, view, partial, render
@param Field context
options[:collection_if] = ->(ctx) { ctx.object.enabled? }
options[:collection_if] = :enabled?
def enabled?(ctx) = ctx.object.enabled?
WidgetBluerpint.render(widget, collection_if: :enabled?).to_json
unless
Inverse of if.
field_unless
Inverse of field_if.
object_unless
Inverse of object_if.
collection_unless
Inverse of collection_if.
Field mapping
These options let you change how fields values are extracted from your objects.
from
Populate the field using a method/Hash key other than the field name.
Available in field, object, collection
field :desc, from: :description
extractor
Pass a custom extractor class or instance.
Available in field, object, collection
# Pass as a class
object :category, CategoryBlueprint, extractor: MyCategoryExtractor
# or an instance
object :category, CategoryBlueprint, extractor: MyCategoryExtractor.new(args)
Note that when you pass a class, it will be initialized once per render.
Metadata
These options allow you to add metadata to the rendered output.
root
Pass a root key to wrap the output.
Available in blueprint, view, partial, render
options[:root] = :data
WidgetBlueprint.render(widget, root: :data).to_json
meta
Add a meta key and data to the wrapped output (requires the root option).
Available in blueprint, view, partial, render
@param Result context
options[:root] = :data
options[:meta] = { page: 1 }
# If you pass a Proc/lambda, it can call instance methods defined on the Blueprint
options[:meta] = ->(ctx) { { page: page_num(ctx) } }
WidgetBlueprint
.render(widget, root: :data, meta: { page: params[:page] })
.to_json
Extensions
Blueprinter has a powerful extension system with hooks for every step of the serialization lifecycle. Some are included with Blueprinter, others are available as gems, and you can easily write your own using the Extension API.
Using extensions
Extensions can be added to your ApplicationBlueprint or any other blueprint, view, or partial. They're inherited from parent classes and views, but can be overridden.
class MyBlueprint < ApplicationBlueprint
# This extension instance will exist for the duration of your program
extensions << FooExtension.new
# These extensions will be initialized once during each render
extensions << BarExtension
extensions << -> { ZorpExtension.new(some_args) }
# Inline extensions are also initialized once per render
extension do
def blueprint_output(ctx) = ctx.result.merge({ foo: "Foo" })
end
view :minimal do
# extensions is a simple Array, so you can add or remove elements
extensions.select! { |ext| ext.is_a? FooExtension }
# or simply replace the whole Array
self.extensions = [FooExtension.new]
end
end
Included extensions
These extensions are distributed with Blueprinter. Simply add them to your configuration.
Field Order
Control the order of fields in your output. See Fields API for more information about the block parameters.
extensions << Blueprinter::Extensions::FieldOrder.new { |a, b| a.name <=> b.name }
MultiJson
The MultiJson extension switches Blueprinter from Ruby's built-in JSON library to the multi_json gem. Just install the multi_json gem, your serialization library of choice, and enable the extension.
extensions << Blueprinter::Extensions::MultiJson.new
# Any options you pass will be forwarded to MultiJson.dump
extensions << Blueprinter::Extensions::MultiJson.new(pretty: true)
# You can also pass MultiJson.dump options during render
WidgetBlueprint.render(widget, multi_json: { pretty: true }).to_json
If multi_json doesn't support your preferred JSON library, you can use Blueprinter's json extension hook to render JSON however you like.
OpenTelemetry
Enable the OpenTelemetry extension to see what's happening while you render your blueprints. One outer blueprinter.render span will nest various blueprinter.object and blueprinter.collection spans. Each span will include the blueprint/view name that triggered it.
Extension hooks will be wrapped in blueprinter.extension spans and annotated with the current extension and hook name.
extensions << Blueprinter::Extensions::OpenTelemetry.new("my-tracer-name")
ViewOption
The ViewOption extension uses the blueprint extension hook to add a view option to render, render_object, and render_collection. It allows V1-compatible rendering of views.
extensions << Blueprinter::Extensions::ViewOption.new
Now you can render a view either way:
# V2 style
MyBlueprint[:foo].render(obj)
# or V1 style
MyBlueprint.render(obj, view: :foo)
Gem extensions
Have an extension you'd like to share? Let us know and we may add it to the list!
blueprinter-activerecord
blueprinter-activerecord is an official extension from the Blueprinter team providing ActiveRecord integration, including automatic preloading of associations based on your Blueprint definitions.
Rendering
Rendering to JSON
WidgetBlueprint.render(widget).to_json
If you're using Rails, you may omit .to_json when calling render json:
render json: WidgetBlueprint.render(widget)
Ruby's built-in JSON library is used by default. Alternatively, you can use the built-in MultiJson extension. Or for total control, implement the json extension hook and call any serializer you like.
Rendering to a Hash
WidgetBlueprint.render(widget).to_hash
Rendering a view
# Render a view
WidgetBlueprint[:extended].render(widget).to_json
# Render a nested view
WidgetBlueprint["extended.price"].render(widget).to_json
# These two both render the default view
WidgetBlueprint.render(widget).to_json
WidgetBlueprint[:default].render(widget).to_json
Passing options
An options hash can be passed to render. Read more about options.
WidgetBlueprint.render(Widget.all, exclude_if_nil: true).to_json
Rendering collections
render will treat any Enumerable, except Hash, as an array of objects:
WidgetBlueprint.render(Widget.all).to_json
If you wish to be explicit you may use render_object and render_collection:
WidgetBlueprint.render_object(widget).to_json
WidgetBlueprint.render_collection(Widget.all).to_json
Whatever you pass to render_collection must respond to map, yielding zero or more serializable objects, and returning an Enumerable with the mapped results.
Blueprinter API
Blueprinter has a rich API for extending the serialization process and reflecting on your blueprints.
Extensions
The extensions API offers deep hooks into the serialization process. Read more.
Reflection
The reflection API allows your application, or Blueprinter extensions, to introspect on your blueprints' options, fields, and views. Read more.
Extractors
By creating and using custom extractors, you can change the way field values are extracted from objects. Read more.
Context Objects
Context objects are the arguments you'll receive in most of the above APIs. Read more.
Fields
Several APIs provide access to structs describing field definitions. Read more.
Extensions
Blueprinter has a powerful extension system with hooks for every step of the serialization lifecycle. In fact, many of Blueprinter's features are implemented as built-in extensions!
Simply extend the Blueprinter::Extension class, define the hooks you need, and add it to your configuration.
class MyExtension < Blueprinter::Extension
# Use the exclude_field? hook to exclude certain fields on Tuesdays
def exclude_field?(ctx) = ctx.field.options[:tues] == false && Date.today.tuesday?
end
class MyBlueprint < ApplicationBlueprint
extensions << MyExtension.new
end
Alternatively, you can define an extension direclty in your blueprint:
class MyBlueprint < ApplicationBlueprint
extension do
def exclude_field?(ctx) = ctx.field.options[:tues] == false && Date.today.tuesday?
end
end
Hooks
Hooks are called in the following order. They are passed a context object as an argument.
- blueprint
- blueprint_fields
- blueprint_setup
- around_serialize_object | around_serialize_collection
- json
Additionally, the around_hook hook runs around all other hooks.
Chain vs override hooks
Most hooks are chained; if you have N of the same hook, they run one after the other, using the output of one as input for the next. However, a few hooks are override hooks: only the last one runs. Override hooks are used to replace built-in functionality, like the JSON serializer.
blueprint
Override hook
@param Render Context NOTEfieldswill be empty
@return Class The Blueprint class to use
@cost Low - run once during render
Return a different blueprint class to render with. If multiple extensions define this hook, only the last one will be used. The included, optional View Option extension uses this hook.
The following example looks for a view option passed in to render. If present, it attempts to return a child view.
def blueprint(ctx)
view = ctx.options[:view]
view ? ctx.blueprint.class[view] : ctx.blueprint.class
end
blueprint_fields
Override hook
@param Render Context
@return Array<Field> The fields to serialize
@cost Low - run once for every blueprint class during render
Customize the order fields are rendered in - or strip out certain fields entirely. If multiple extensions define this hook, only the last one will be used. The included, optional Field Order extension uses this hook.
In this hook, context.fields will contain all of the view's fields in the order in which they were defined. (Fields from used partials are appended.) The fields this hook returns are used as context.fields in all subsequent hooks:
The following example removes all collection fields and sorts the rest by name:
def blueprint_fields(ctx)
ctx.fields.
reject { |f| f.type == :collection }.
sort_by(&:name)
end
It's run once per blueprint class during a render. So if you're rendering an array of widgets with WidgetBlueprint, which contains PartBlueprints and CategoryBlueprints, this hook will be called three times: one for each of those blueprints.
blueprint_setup
@param Render Context
@cost Low - run once for every blueprint class during render
Allows an extension to perform setup operations for the render of the current blueprint.
def blueprint_setup(ctx)
# do setup for ctx.blueprint
end
It's run once per blueprint class during a render. So if you're rendering an array of widgets with WidgetBlueprint, which contains PartBlueprints and CategoryBlueprints, this hook will be called three times: one for each of those blueprints.
around_serialize_object
@param Object Context
context.objectwill contain the current object being rendered
@cost Medium - run every time any blueprint is rendered
Wraps the rendering of every object (context.object). This could be the top-level object or one from an association N levels deep (check context.depth).
Rendering happens during yield, allowing the hook to run code before and after the render. If yield is not called exactly one time, a BlueprinterError is thrown.
def around_serialize_object(ctx)
# do something before render
yield # render
# do something after render
end
around_serialize_collection
@param Object Context
context.objectwill contain the current collection being rendered
@cost Medium - run every time any blueprint is rendered
Wraps the rendering of every collection (context.object). This could be the top-level collection or one from an association N levels deep (check context.depth).
Rendering happens during yield, allowing the hook to run code before and after the render. If yield is not called exactly one time, a BlueprinterError is thrown.
def around_serialize_collection(ctx)
# do something before render
yield # render
# do something after render
end
object_input
@param Object Context
context.objectwill contain the current object being rendered
@return Object A new or modified version ofcontext.object
@cost Medium - run every time an object is rendered
Runs before serialization of any object from render, render_object, or a blueprint's object field. You may modify and return context.object or return a different object entirely. Whatever object is returned will be used as context.object in subsequent hooks, then rendered.
If you want to target only the root object, check context.depth == 1.
def object_input(ctx)
ctx.object
end
collection_input
@param Object Context
context.objectwill contain the current collection being rendered
@return Object A new or modified version ofcontext.object, which will be array-like
@cost Medium - run every time a collection is rendered
Runs before serialization of any collection from render, render_collection, or a blueprint's collection field. You may modify and return context.object or return a different collection entirely. Whatever collection is returned will be used as context.object in subsequent hooks, then rendered.
If you want to target only the root collection, check context.depth == 1.
def collection_input(ctx)
ctx.object
end
blueprint_input
@param Object Context
context.objectwill contain the current object being rendered
@return Object A new or modified version ofcontext.object
@cost Medium - run every time any blueprint is rendered
Run each time a blueprint renders, allowing you to modify or return a new object (context.object) used for the render. For collections of size N, it will be called N times. Whatever object is returned will be used as context.object in subsequent hooks, then rendered.
def blueprint_input(ctx)
ctx.object
end
extract_value
Override hook
@param Field Contextcontext.fieldwill contain the current field being serialized, andcontext.objectthe current object
@return Object The value for the field
@cost High - run for every field, object, and collection
Called on each field, object, and collection to extract a field's value from an object. The return value is used as context.value in subsequent hooks. If multiple extensions define this hook, only the last one will be used.
def extract_value(ctx)
ctx.object.public_send(ctx.field.from)
end
field_value
@param Field Context
context.fieldwill contain the current field being serialized, andcontext.objectthe current object
@return Object The value to be rendered
@cost High - run for every field (not object or collection fields)
Run after a field value is extracted from context.object. The extracted value is available in context.value. Whatever value you return is used as context.value in subsequent field_value hooks, then run through any formatters and rendered.
def field_value(ctx)
case ctx.value
when String then ctx.value.strip
else ctx.value
end
end
object_field_value
@param Field Context
context.fieldwill contain the current field being serialized, andcontext.objectthe current object
@return Object The object to be rendered for this field
@cost High - run for every object field
Run after an object field value is extracted from context.object. The extracted value is available in context.value. Whatever value you return is used as context.value in subsequent object_field_value hooks, then rendered.
def object_field_value(ctx)
ctx.value
end
collection_field_value
@param Field Context
context.fieldwill contain the current field being serialized, andcontext.objectthe current object
@return Object The array-like collection to be rendered for this field
@cost High - run for every collection field
Run after a collection field value is extracted from context.object. The extracted value is available in context.value. Whatever value you return is used as context.value in subsequent collection_field_value hooks, then rendered.
def collection_field_value(ctx)
ctx.value.compact
end
exclude_field?
@param Field Context
context.fieldwill contain the current field being serialized, andcontext.objectthe current object
@return Boolean Truthy to exclude the field from the output
@cost High - run for every field (not object or collection fields)
If any extension with this hook returns truthy, the field will be excluded from the output. The formatted field value is available in context.value.
def exclude_field?(ctx)
ctx.field.options[:tuesday] == false && Date.today.tuesday?
end
exclude_object_field?
@param Field Context
context.fieldwill contain the current field being serialized, andcontext.objectthe current object
@return Boolean Truthy to exclude the field from the output
@cost High - run for every object field
If any extension with this hook returns truthy, the object field will be excluded from the output. The field object value is available in context.value.
def exclude_object_field?(ctx)
ctx.field.options[:tuesday] == false && Date.today.tuesday?
end
exclude_collection_field?
@param Field Context
context.fieldwill contain the current field being serialized, andcontext.objectthe current object
@return Boolean Truthy to exclude the field from the output
@cost High - run for every collection field
If any extension with this hook returns truthy, the collection field will be excluded from the output. The field collection value is available in context.value.
def exclude_collection_field?(ctx)
ctx.field.options[:tuesday] == false && Date.today.tuesday?
end
field_result
@param Field Context
context.fieldwill contain the current field being serialized, andcontext.objectthe current object
@return Object The value to be rendered for this field
@cost High - run for every field
The final value to be used for the field, available in context.value. You may modify or replace it. Whatever value you return is used as context.value in subsequent hooks, then rendered. Not called if exclude_field? returned true.
def field_result(ctx)
ctx.value
end
object_field_result
@param Field Context
context.fieldwill contain the current field being serialized, andcontext.objectthe current object
@return Object The value to be rendered for this field
@cost High - run for every field
The final value to be used for the field, available in context.value. You may modify or replace it. Whatever value you return is used as context.value in subsequent hooks, then rendered. Not called if exclude_object_field? returned true.
def object_field_result(ctx)
ctx.value
end
collection_field_result
@param Field Context
context.fieldwill contain the current field being serialized, andcontext.objectthe current object
@return Object The value to be rendered for this field
@cost High - run for every field
The final value to be used for the field, available in context.value. You may modify or replace it. Whatever value you return is used as context.value in subsequent hooks, then rendered. Not called if exclude_collection_field? returned true.
def collection_field_result(ctx)
ctx.value
end
blueprint_output
@param Result Context
context.resultwill contain the serialized Hash from the current blueprint, andcontext.objectthe current object
@return Hash The Hash to use as this blueprint's serialized output
@cost Medium - run every time any blueprint is rendered
Run after a blueprint serializes an object to a Hash, allowing you to modify the output. The Hash is available in context.result. For collections of size N, it will be called N times. Whatever Hash is returned will be used as context.result in subsequent hooks and used as the serialized output for this blueprint.
def blueprint_output(ctx)
ctx.result.merge(ctx.object.extra_fields)
end
object_output
@param Result Context
context.resultwill contain the serialized Hash from the current blueprint, andcontext.objectthe current object
@return [Object] The value to use for the fully serialized object
@cost High - run for every object field
Run after an object is fully serialized. This may be the root object from render or an object field from a blueprint (check context.depth). This example wraps the result in a metadata block:
def object_output(ctx)
{ data: ctx.value, metadata: {...} }
end
collection_output
@param Result Context
context.resultwill contain the array of serialized Hashes from the current blueprint, andcontext.objectthe current collection
@return Object The value to use for the fully serialized collection
@cost High - run for every collection field
Run after a collection is fully serialized. This may be the root collection from render or a collection field from a blueprint (check context.depth). This example wraps the result in a metadata block:
def collection_output(ctx)
{ data: ctx.value, metadata: {...} }
end
json
Override hook
@param Result Contextcontext.resultwill contain the serialized Hash or array from the top-level blueprint, andcontext.objectthe top-level object or collection
@return String The JSON output
@cost Low - run once per JSON render
Serializes the final output to JSON. Only called on the top-level blueprint. If multiple extensions define this hook, only the last one will be used.
The default behavior looks like:
def json(ctx)
JSON.dump ctx.result
end
around_hook
@param Hook Context
@cost Variable - Depends on what hooks your extensions implement
A special hook that runs around all other extension hooks. Useful for instrumenting. You can exclude an extension's hooks from this hook by putting def hidden? = true in the extension.
def around_hook(ext, hook)
# Do something before extension hook runs
yield # hook runs here
# Do something after extension hook runs
end
Reflection
Blueprints may be reflected on to inspect their views, fields, and options. This is useful for building extensions, and possibly even for some applications.
We will use the following blueprint in the examples below:
class WidgetBlueprint < ApplicationBlueprint
field :name
field :description, exclude_if_empty: true
object :category, CategoryBlueprint
collection :parts, PartBlueprint
view :extended do
object :manufacturer, CompanyBlueprint[:full]
view :with_price do
field :price
end
end
end
Blueprint & view names
WidgetBlueprint.blueprint_name
=> "WidgetBlueprint"
WidgetBlueprint.view_name
=> :default
WidgetBlueprint[:extended].blueprint_name
=> "WidgetBlueprint.extended"
WidgetBlueprint[:extended].view_name
=> :extended
WidgetBlueprint["extended.with_price"].blueprint_name
=> "WidgetBlueprint.extended.with_price"
WidgetBlueprint["extended.with_price"].view_name
=> :"extended.with_price"
Blueprint & view options
WidgetBlueprint.options
=> {exclude_if_nil: true}
WidgetBlueprint[:extended].options
=> {exclude_if_nil: true, exclude_if_empty: true}
Views
Here, :default refers to the top level of the blueprint.
WidgetBlueprint.reflections.keys
=> [:default, :extended, :"extended.with_price"]
You can also reflect directly on a view.
WidgetBlueprint[:extended].reflections.keys
=> [:default, :with_price]
Notice that the names are relative: :default now refers to the :extended view, since we called .reflections on :extended. The prefix is also gone from the nested :with_price view.
Fields
view = WidgetBlueprint.reflections[:default]
# Regular fields
view.fields.keys
=> [:name, :description]
# Object fields
view.objects.keys
=> [:category]
# Collection fields
view.collections.keys
=> [:parts]
# All fields in the order they were defined
view.ordered
# returns an array of field objects
Field metadata
view = WidgetBlueprint.reflections[:default]
field = view.fields[:description]
field.name
=> :description
field.from
=> :description # the :from option in the DSL
field.value_proc
=> nil # the block you passed to the field, if any
field.options # all other options passed to the field
=> { exclude_if_empty: true }
Object and collection fields have the same metadata as regular fields, plus a blueprint attribute:
view = WidgetBlueprint.reflections[:default]
field = view.collections[:parts]
# it returns the Blueprint class, so you can continue reflecting
field.blueprint
=> PartBlueprint
field.blueprint.reflections[:default].fields
=> # array of fields on the default view of PartBlueprint
If you used a view in an object or collection field, you can reflect on that view just like a blueprint:
view = WidgetBlueprint.reflections[:extended]
field = view.objects[:manufacturer]
field.blueprint.to_s
=> "CompanyBlueprint.full"
# Remember, we're reflecting ON the :full view, so the name is relative!
field.blueprint.reflections[:default].fields
=> # array of fields on the :full view of CompanyBlueprint
Extractors
Extractors are extensions that pull field values from the objects you're serializing. The default extraction logic is smart enough for most use cases, but you can create custom extractors if needed. (Note that passing a block to a field completely bypasses extractors.)
Default Extractor
The default extractor is a built-in extension. If context.object is a Hash, it tries symbol, then string keys. Otherwise, it calls public_send on the object.
class Blueprinter::Extensions::Core::Extractor < Blueprinter::Extension
def extract_value(ctx)
if ctx.object.is_a? Hash
ctx.object[ctx.field.from] || ctx.object[ctx.field.from_str]
else
ctx.object.public_send(ctx.field.from)
end
end
end
Custom Extractors
Your extract_value hook will be passed a Field context object.
class WeirdObjectExtractor < Blueprinter::Extension
def extract_value(ctx)
# my extraction logic
end
end
There are several ways to use your extractor:
- Add it to your blueprint(s) or view(s) like any other extension.
- Add it to specific fields using the extractor option.
Context Objects
Context objects are the arguments passed to APIs like field blocks, option procs and extension hooks. There are several kinds of context objects, each with its own set of fields.
Render Context
blueprint
The current Blueprint instance. You can use this to access the Blueprint's name, options, reflections, and instance methods.
fields
A frozen array of field definitions that will be serialized, in order. See Fields API and the blueprint_fields hook.
options
The frozen options Hash passed torender. An empty Hash if none was passed.
depth
The current blueprint depth (1-indexed).
Object Context
blueprint
The current Blueprint instance. You can use this to access the Blueprint's name, options, reflections, and instance methods.
fields
A frozen array of field definitions that will be serialized, in order. See Fields API and the blueprint_fields hook.
options
The frozen options Hash passed torender. An empty Hash if none was passed.
object
The object or collection currently being serialized.
depth
The current blueprint depth (1-indexed).
Field Context
blueprint
The current Blueprint instance. You can use this to access the Blueprint's name, options, reflections, and instance methods.
fields
A frozen array of field definitions that will be serialized, in order. See Fields API and the blueprint_fields hook.
options
The frozen options Hash passed torender. An empty Hash if none was passed.
object
The object currently being serialized.
field
A struct of the field, object, or collection currently being rendered. You can use this to access the field's name and options. See Fields API.
value
The extracted field value. (In certain situations, like the extractor API and field blocks, it will always benilsince nothing has been extracted yet.)
depth
The current blueprint depth (1-indexed).
Result Context
blueprint
The current Blueprint instance. You can use this to access the Blueprint's name, options, reflections, and instance methods.
fields
A frozen array of field definitions that were serialized, in order. See Fields API and the blueprint_fields hook.
options
The frozen options Hash passed torender. An empty Hash if none was passed.
object
The object or collection that was just serialized.
result
A serialized result. Depending on the situation this will be a Hash or an array of Hashes.
depth
The current blueprint depth (1-indexed).
Hook Context
blueprint
The current Blueprint instance. You can use this to access the Blueprint's name, options, reflections, and instance methods.
fields
A frozen array of field definitions that will be serialized, in order. See Fields API and the blueprint_fields hook.
options
The frozen options Hash passed torender. An empty Hash if none was passed.
extension
Instance of the current extension
hook
Name of the current hook
depth
The current blueprint depth (1-indexed).
Fields
Extensions, reflection, and other APIs allow access to structs that describe fields.
type
Symbol:field | :object | :collection
The type of field.
name
Symbol
Name of the field as it will appear in the JSON or Hash output.
from
Symbol
Name of the field in the source object (usually the same asname).
from_str
String
Same asfrom, but as a frozen string.
value_proc
nil | Proc
The block passed to the field definition (if given). Expects a Field Context argument and returns the field value.
options
Hash
A frozen Hash of any additional options passed to the field.
blueprint (object and collection only)
Class
The blueprint to use for serializaing the object or collection.
Upgrading to API V2
You have two options when updating from the legacy/V1 API: full update or incremental update.
Regardless which you choose, you'll need to familiarize yourself with the new DSL and API. The rest of this section will focus on the differences between V1 and V2.
Full update
Update blueprinter to 2.x. All of your blueprints will need updated to use the new DSL. If you're making use of extensions, custom extractors, or transformers, they'll also need updated to the new API.
Incremental update
Larger applications may find it easier to update incrementally. Update blueprinter to 1.2.x, which contains both the legacy/V1 and V2 APIs. They can be used side-by-side.
# A legacy/V1 blueprint
class WidgetBlueprint < Blueprinter::Blueprint
field :name
view :with_desc do
field :description
end
view :with_category do
# Using a V2 blueprint in a legacy/V1 blueprint
association :category, blueprint: CategoryBlueprint, view: :extended
end
end
# A V2 blueprint
class CategoryBlueprint < ApplicationBlueprint
field :name
view :extended do
# Using a legacy/V1 blueprint in a V2 blueprint
collection :widgets, WidgetBlueprint[:with_desc]
end
end
Configuration
Blueprinter V2 has no concept of global configruation like V1's Blueprinter.configure. Instead, blueprints and views inherit configuration from their parent classes. By putting your "global" configuration into ApplicationBlueprint, all your application's blueprints and views will inherit it.
class ApplicationBlueprint < Blueprinter::Blueprint
options[:exclude_if_nil] = true
extensions << MyExtension.new
end
Read more about options and extensions.
Overrides
Child classes, views, and partials can override their inherited configuration.
class MyBlueprint < ApplicationBlueprint
options[:exclude_if_nil] = false
view :foo do
options.clear
extensions.clear
end
end
Customization
Formatting
Blueprinter V2 has a more generic approach to formatting, allowing any type of value to have formatting applied. Learn more.
format(Date) { |date| date.iso8601 }
The field_value, object_field_value, and collection_field_value extension hooks can also be used.
Custom extractors
Custom extraction in V2 is accomplished using the extract_value extension hook.
Fields, objects, and collections continue to have an extractor option. Simply pass your extension class to it. Learn more.
Unlike Legacy/V1, custom extractors do not override blocks passed to fields, objects, and collections. If a field has a block, that's how it's extracted.
Transformers
Blueprinter V2's extension hooks offer many ways to transform your inputs and outputs. The blueprint_output hook offers equivalent functionality to Legacy/V1 transformers.
Fields
Identifier field and view
Blueprinter Legacy/V1 had a special feature for an id field and identifier view. Blueprinter V2 does not have this concept, but you can simulate it in your ApplicationBlueprint.
class ApplicationBlueprint < Blueprinter::Blueprint
# Every Blueprint that inherits from ApplicationBlueprint will have this field
field :id
# Every Blueprint that inherits from ApplicationBlueprint will have this view,
# and it will only have the `id` field
view :identifier, empty: true do
field :id
end
end
Renaming fields
In Blueprinter Legacy/V1, you could rename fields using the name option. Blueprinter V2 swaps the order and uses from. We believe this makes your blueprints more readable.
In the following examples, both blueprints are populating the output field description from a source attribute named desc.
# Legacy/V1
field :desc, name: :description
# V2
field :description, from: :desc
Associations
Blueprinter Legacy/V1 figured out if associations were single items or arrays at runtime. Blueprinter V2 accounts for this in the DSL. Also, the :blueprint and :view options are gone.
class WidgetBlueprint < ApplicationBlueprint
field :name
object :category, CategoryBlueprint
collection :parts, PartBlueprint
# specify a view
object :manufacturer, CompanyBlueprint[:extended]
end
Field order
Blueprinter Legacy/V1 offered two options for ordering fields: :name_asc (default), and :definition (order they were defined in). Blueprinter V2 defaults to the order of definition. You can define a different order using the blueprint_fields extension hook or the built-in FieldOrder extension.
The following replicates Legacy/V1's default field order using the built-in FieldOrder extension.
class ApplicationBlueprint < Blueprinter::Blueprint
extensions << Blueprinter::Extensions::FieldOrder.new do |a, b|
if a.name == :id
-1
elsif b.name == :id
1
else
a.name <=> b.name
end
end
end
Rendering
You can read the full rendering documentation here. This page highlights the main differences between V1 and V2.
Rendering to JSON
If you're using Rails's render json:, V2 blueprints should continue to work like Legacy/V1:
render json: WidgetBlueprint.render(widget)
Otherwise, it now looks like this:
WidgetBlueprint.render(widget).to_json
Rendering to Hash
WidgetBlueprint.render(widget).to_hash
Views
V2's preferred method of rendering views is:
WidgetBlueprint[:extended].render(widget).to_json
However, the ViewOption extension can be enabled to allow V1-style view rendering:
WidgetBlueprint.render(widget, view: :extended).to_json
Reflection
The V2 Reflection API has very few changes from Legacy/V1.
Reflecting on fields
Regular fields (no change):
MyBlueprint.reflections[:default].fields
Objects and collections:
# Legacy/V1 does not differentiate between objects and collections
MyV1Blueprint.reflections[:default].associations
# V2 does
MyV2Blueprint.reflections[:default].objects
MyV2Blueprint.reflections[:default].collections
Field names
V2's field metadata is similar, but there's an important different in name.
Legacy/V1
In Legacy/V1, name refers to what the field is called in the input.
class MyV1Blueprint < Blueprinter::Base
field :foo, name: :bar
end
ref = MyV1Blueprint.reflections[:default]
# What the field is called in the source object
ref.fields[:foo].name
=> :foo
# What the field will be called in the output
ref.fields[:foo].display_name
=> :bar
V2
In V2, name refers to what the field is called in the output. Note that this change is also reflected in the Hash key.
class MyV2Blueprint < Blueprinter::Blueprint
field :bar, from: :foo
end
ref = MyV1Blueprint.reflections[:default]
# What the field will be called in the output
ref.fields[:bar].name
=> :bar
# What the field is called in the source object
ref.fields[:bar].from
=> :foo
Extensions
The V2 Extension API, as well as the DSL for enabling V2 extensions, are vastly different and more powerful than V1. Legacy/V1 had only one extension hook: pre_render. V2 has over a dozen.
Porting pre_render
Legacy/V1's pre_render hook does not exist in V2, but it has three possible replacements:
- object_input intercept an object before it's serialized
- collection_input intercept a collection before it's serialized
- blueprint_input runs each time a blueprint serializes an object
Legacy/V1 Docs
TODO copy from old README