fast_jsonapi icon indicating copy to clipboard operation
fast_jsonapi copied to clipboard

Implement error object serializer

Open pcriv opened this issue 7 years ago • 10 comments

http://jsonapi.org/format/#error-objects http://jsonapi.org/examples/#error-objects

pcriv avatar Feb 26 '18 10:02 pcriv

@pablocrivella even in those links jsonapi is very vague about what error objects needed to look like almost every attribute is a MAY have. I would appreciate ideas on what the developer interface should be for specifying error objects?

shishirmk avatar Mar 18 '18 23:03 shishirmk

what about this for the interface:

class ErrorSerializer
  include FastJsonapi::ErrorSerializer
  set_id :id # defaults to :id, set to false to disable, can take a block
  link(:about) {|object| "http://movies.com/errors/#{object.type}"}
  status :status # defaults to :status, set to false to disable, can take a block
  code :code # defaults to :code, set to false to disable, can take a block
  title :title #defaults to :title, set to false to disable, can take a block
  detail :detail # defaults to :detail, set to false to disable, can take a block
  source_method :source # defaults to :source, set to false to disable, can take block, expected to be a hash
end

Which would produce:

{
  "errors":[
    {
      "id": "123",
      "status": "422",
      "code": "Bad_Include",
      "title": "You requested an include that isn't allowed",
      "detail": "you requested that we include the \"tv_show\" association, however that isn't allowed",
      "source":{
        "pointer": "params/include",
        "parameter": "include"
      },
      "links":{
        "about": "http://movies.com/errors/bad_include"
      }
    }
  ]
}

Obviously the other option is to default to not showing and have a value of true be the auto value

ChrisHampel avatar Jul 18 '18 20:07 ChrisHampel

@shishirmk I think the only thing needed from the gem is to be able to define root key as "errors" instead of "data", etc.

Errors themselves look just like a regular serializer

ababich avatar Jul 19 '18 04:07 ababich

the reason I was suggesting a sperate serializer base, is that, as there is a specific list of allowed base attributes, and most attributes, are specified to be strings only, additional optimization should be possible

ChrisHampel avatar Jul 19 '18 10:07 ChrisHampel

Here is a quick and dirty extension of ObjectSerializer that basically achieves what proposed above:

require 'fast_jsonapi'

module FastJsonapi::ErrorSerializer
  extend ActiveSupport::Concern

  included do
    attr_accessor :with_root_key

    include FastJsonapi::ObjectSerializer
    set_id :title

    def initialize(resource, options = {})
      super
      @with_root_key = options[:with_root_key] != false
    end

    def hash_for_one_record
      serialized_hash = super[:data]&[:attributes]
      return !with_root_key ? serialized_hash : {
        errors: serialized_hash
      }
    end

    def hash_for_collection
      serialized_hash = super[:data]&.map{|err| err[:attributes]}
      return !with_root_key ? serialized_hash : {
        errors: serialized_hash
      }
    end
  end
end

If you are using Rails, you can put this code into lib/fast_jsonapi/error_serializer.rb and require it from an initializer the usual way:

config.autoload_paths += Dir[Rails.root.join("lib/fast_jsonapi/*.rb")].each {|l| require l }

Given an error class (custom serialzier_source method omitted):

class RequestErrorSerializer
  include FastJsonapi::ErrorSerializer

  attributes  :title,
              :detail,
              :code,
              :status

  attribute :source do |err|
    err.serializer_source
  end
end

It's a rather hacky solution (especially in that set_id :title) and I'd also love to see native support for this.

coconup avatar Aug 02 '18 16:08 coconup

I would also like to see how a native support for this would look like. @coconup, thanks for the example but I'm still a novice and didn't catch what would be expected exactly in serializer_source, which you omitted. Would you please provide an example?

malaquf avatar Oct 02 '18 15:10 malaquf

@malaquf I am passing instances of a custom error class to the serializer, which have a serializer_source attribute. It could also be a method if you need some extra logic. Something like this:

class CustomError
  attr_accessor :code, :detail, :title, :status, :serializer_source
  def initialize(source:, code:, detail: nil, title:, status: 400)
    @serializer_source = source
    @code = code
    @status = status
    ...
  end
end

When I want to serialize an error:

error = CustomError.new(source: 'my_source', title: 'an error occurred', code: 'my_error')
render json: RequestErrorSerializer.new(error).serialized_json, status: 422

coconup avatar Oct 02 '18 15:10 coconup

Just wanted to mention what I already wrote in the #175, that these two issues should be handled together. Also, it would be nice to get an update from the team if there's any interest to provide rails-specific features like this one.

stas avatar Jan 14 '19 16:01 stas

I come up with my own serializer

class ErrorSerializer
  attr_reader :resource, :errors
  alias_method :serializable_hash, :as_json
  alias_method :serialized_json, :to_json

  def initialize(resource, options = {})
    @resource = resource
    @errors = errors_for(resource).flatten
  end

  def id
    resource.id.to_s
  end

  def type
    resource.class.name.downcase
  end

  def serializable_hash(options = nil)
    {}.tap do |hash|
      hash[:id] = id if has_id?
      hash[:type] = type if has_id?
      hash[:errors] = errors
    end
  end

  def serialized_json(options = nil)
    serializable_hash.to_json(options)
  end

  private

  def has_id?
    @resource.respond_to?(:id)
  end

  def errors_for(resource)
    resource.errors.messages.map do |field, errors|
      build_hashes_for(field, errors)
    end
  end

  def build_hashes_for(field, errors)
    errors.map do |error_message|
      build_hash_for(field, error_message)
    end
  end

  def build_hash_for(field, error_message)
    {}.tap do |hash|
      hash[:source] = {pointer: "/data/attributes/#{field}"}
      hash[:detail] = error_message
    end
  end
end

Then I override render method in a concern:

module ActsAsJsonApiController
  extend ActiveSupport::Concern

  included do
    def render(options = {})
      case options[:json]
      when ActiveRecord::Base, ApplicaionForm
        apply_item_options!(options)
      when ActiveRecord::Relation
        apply_collection_options!(options)
      end

      super(options)
    end

    def serializer
      message = "#{self.class} must implement `serializer` method"
      raise NotImplementedError, message
    end

    def apply_item_options!(options = {})
      if options[:json].valid?
        options[:json] = serializer.new(options[:json], options)
      else
        options[:include] = nil # if removed raises an exception
        options[:status] = :bad_request
        options[:json] = ErrorSerializer.new(options[:json], options).serializable_hash
      end
    end

    def apply_collection_options!(options = {})
      options[:meta] = {} unless options.key?(:meta)
      options[:meta][:pagination] = build_pagination_metadata(options[:json])
      options[:json] = serializer.new(options[:json], options)
      options[:is_collection] = true
    end

    def build_pagination_metadata(collection)
      {
        total: collection.klass.count,
        current_page: collection.current_page,
        prev_page: collection.prev_page,
        next_page: collection.next_page,
        total_pages: collection.total_pages,
      }
    end
  end
end

ali-sennalabs avatar Nov 30 '19 07:11 ali-sennalabs

It seems Netflix has abandoned this project. The community created a new fork to continue supporting this project. Please refer

https://github.com/fast-jsonapi/fast_jsonapi

On Sat, 30 Nov, 2019, 12:55 PM ali-sennalabs, [email protected] wrote:

I come up with my own serializer

class ErrorSerializer attr_reader :resource, :errors alias_method :serializable_hash, :as_json alias_method :serialized_json, :to_json

def initialize(resource, options = {}) @resource = resource @errors = errors_for(resource).flatten end

def id resource.id.to_s end

def type resource.class.name.downcase end

def serializable_hash(options = nil) {}.tap do |hash| hash[:id] = id if has_id? hash[:type] = type if has_id? hash[:errors] = errors end end

def serialized_json(options = nil) serializable_hash.to_json(options) end

private

def has_id? @resource.respond_to?(:id) end

def errors_for(resource) resource.errors.messages.map do |field, errors| build_hashes_for(field, errors) end end

def build_hashes_for(field, errors) errors.map do |error_message| build_hash_for(field, error_message) end end

def build_hash_for(field, error_message) {}.tap do |hash| hash[:source] = {pointer: "/data/attributes/#{field}"} hash[:detail] = error_message end endend

Then I override render method in a concern:

module ActsAsJsonApiController extend ActiveSupport::Concern

included do def render(options = {}) case options[:json] when ActiveRecord::Base, ApplicaionForm apply_item_options!(options) when ActiveRecord::Relation apply_collection_options!(options) end

  super(options)
end

def serializer
  message = "#{self.class} must implement `serializer` method"
  raise NotImplementedError, message
end

def apply_item_options!(options = {})
  if options[:json].valid?
    options[:json] = serializer.new(options[:json], options)
  else
    options[:include] = nil # if removed raises an exception
    options[:status] = :bad_request
    options[:json] = ErrorSerializer.new(options[:json], options).serializable_hash
  end
end

def apply_collection_options!(options = {})
  options[:meta] = {} unless options.key?(:meta)
  options[:meta][:pagination] = build_pagination_metadata(options[:json])
  options[:json] = serializer.new(options[:json], options)
  options[:is_collection] = true
end

def build_pagination_metadata(collection)
  {
    total: collection.klass.count,
    current_page: collection.current_page,
    prev_page: collection.prev_page,
    next_page: collection.next_page,
    total_pages: collection.total_pages,
  }
end

endend

— You are receiving this because you are subscribed to this thread. Reply to this email directly, view it on GitHub https://github.com/Netflix/fast_jsonapi/issues/102?email_source=notifications&email_token=ACEAHZNMINBR3F6YBXZRXWTQWIIQLA5CNFSM4ESMAFE2YY3PNVWWK3TUL52HS4DFVREXG43VMVBW63LNMVXHJKTDN5WW2ZLOORPWSZGOEFP3IFQ#issuecomment-559920150, or unsubscribe https://github.com/notifications/unsubscribe-auth/ACEAHZNYKYOSFUVO5YSP3WLQWIIQLANCNFSM4ESMAFEQ .

kapilnarula avatar Nov 30 '19 07:11 kapilnarula