Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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:

  1. Definitions in the partial
  2. Definitions in the view
  3. 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:

  1. Definitions in the view after use!
  2. Definitions in the partial
  3. Definitions in the view before use!
  4. 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:

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.

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 NOTE fields will 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.

↑ Back to Hooks

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.

↑ Back to Hooks

around_serialize_object

@param Object Context context.object will 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

↑ Back to Hooks

around_serialize_collection

@param Object Context context.object will 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

↑ Back to Hooks

object_input

@param Object Context context.object will contain the current object being rendered
@return Object A new or modified version of context.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

↑ Back to Hooks

collection_input

@param Object Context context.object will contain the current collection being rendered
@return Object A new or modified version of context.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

↑ Back to Hooks

blueprint_input

@param Object Context context.object will contain the current object being rendered
@return Object A new or modified version of context.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

↑ Back to Hooks

extract_value

Override hook
@param Field Context context.field will contain the current field being serialized, and context.object the 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

↑ Back to Hooks

field_value

@param Field Context context.field will contain the current field being serialized, and context.object the 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

↑ Back to Hooks

object_field_value

@param Field Context context.field will contain the current field being serialized, and context.object the 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

↑ Back to Hooks

collection_field_value

@param Field Context context.field will contain the current field being serialized, and context.object the 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

↑ Back to Hooks

exclude_field?

@param Field Context context.field will contain the current field being serialized, and context.object the 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

↑ Back to Hooks

exclude_object_field?

@param Field Context context.field will contain the current field being serialized, and context.object the 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

↑ Back to Hooks

exclude_collection_field?

@param Field Context context.field will contain the current field being serialized, and context.object the 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

↑ Back to Hooks

field_result

@param Field Context context.field will contain the current field being serialized, and context.object the 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

↑ Back to Hooks

object_field_result

@param Field Context context.field will contain the current field being serialized, and context.object the 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

↑ Back to Hooks

collection_field_result

@param Field Context context.field will contain the current field being serialized, and context.object the 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

↑ Back to Hooks

blueprint_output

@param Result Context context.result will contain the serialized Hash from the current blueprint, and context.object the 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

↑ Back to Hooks

object_output

@param Result Context context.result will contain the serialized Hash from the current blueprint, and context.object the 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

↑ Back to Hooks

collection_output

@param Result Context context.result will contain the array of serialized Hashes from the current blueprint, and context.object the 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

↑ Back to Hooks

json

Override hook
@param Result Context context.result will contain the serialized Hash or array from the top-level blueprint, and context.object the 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

↑ Back to Hooks

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:

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 to render. 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 to render. 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 to render. 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 be nil since 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 to render. 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 to render. 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 as name).

from_str
String
Same as from, 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:

Legacy/V1 Docs

TODO copy from old README