Skip to content

Object Constructors


The Serializer component also has the concept of Object Constructors that determine how a new object is constructed during deserialization. A use case could be retrieving the object from the database as part of a PUT request in order to apply the deserialized data onto it. This would allow it to retain the PK, any timestamps, or ASRA::ReadOnly values.


This example uses the Granite ORM, but should work with others.

# Define a custom `ASR::ObjectConstructorInterface` to allow
# sourcing the model from the database as part of `PUT`
# requests, and if the type is a `Granite::Base`.
# Alias our service to `ASR::ObjectConstructorInterface`
# so ours gets injected instead.
@[ADI::Register(alias: ASR::ObjectConstructorInterface)]
class DBObjectConstructor
  include Athena::Serializer::ObjectConstructorInterface

  # Inject `ART::RequestStore` in order to have access to
  # the current request. Also inject `ASR::InstantiateObjectConstructor`
  # to act as our fallback constructor.
  def initialize(
    @request_store : ART::RequestStore,
    @fallback_constructor : ASR::InstantiateObjectConstructor
  ); end

  # :inherit:
  def construct(navigator : ASR::Navigators::DeserializationNavigatorInterface, properties : Array(ASR::PropertyMetadataBase), data : ASR::Any, type)
    # Fallback on the default object constructor if
    # the type is not a `Granite` model.
    unless type <= Granite::Base
      return @fallback_constructor.construct navigator, properties, data, type

    # Fallback on the default object constructor if 
    # the current request is not a `PUT`.
    unless @request_store.request.method == "PUT"
      return @fallback_constructor.construct navigator, properties, data, type

    # Lookup the object from the database; 
    # assume the object has an `id` property.
    entity = type.find data["id"].as_i64

    # Return a `404` error if no record exists with the given ID.
    raise "An item with the provided ID could not be found." unless entity

    # Apply the updated properties to the retrieved record
    entity.apply navigator, properties, data

    # Return the entity

This object constructor could then be used with the RequestBody param converter, assuming the type of the configuration value is Granite::Base.class. For example:

struct RequestBody < ART::ParamConverterInterface
  # Define a customer configuration for this converter.
  # This allows us to provide a `entity` field within the annotation
  # in order to define _what_ entity should be queried for.
  configuration entity : Granite::Base.class

  # ...

# Make the compiler happy when we want to allow any
# Granite entity to be deserializable via the Serializer component.
class Granite::Base
  include ASR::Model

class Article < Granite::Base
  connection "default"
  table "articles"

  column id : Int64, primary: true
  column title : String

class ExampleController < ART::Controller
  @[ARTA::Post(path: "")]
  @[ARTA::View(status: :created)]
  @[ARTA::ParamConverter("article", converter: RequestBody, entity: Article)]
  def new_article(article : Article) : Article
    # Since we have an actual `Article` instance with the updates
    # from the JSON payload already applied,
    # we can simply save and return the article.