Handling & returning validation errors in an JSON API application


#1

Hello everybody! A little context, I’m very intrigued by Lotus and want to build a JSON backend for an Ember.js application. While the frontend application has client side validations to ensure correct user input the backend has to make sure that only valid data enters the application.

I’m looking for a way to handle validation errors and return them to the client if there is a problem.

In a classic web application where Lotus returns HTML to the client it is pretty clear. The view / form displays the errors to the user and also accesses the actions other exposures to pre-fill the form. When Lotus acts as an JSON backend this is a little different. A successful request gets an JSON object with the data and an 200 or 201 status code. A request with invalid parameters gets an 4xx status code with an JSON object containing the errors.

Example for an successful response:

{
  "id": "1",
  "email": "david@strauss.io"
}

Example for an unsuccessful response:

{
  "errors": {
    "email": ["must be set"]
  }
}

I’m struggling to handle this situation in a nice way. You can view my current solution at the bottom of the post. My ideal solution is to halt the request or throw an exception so I can handle this scenario in one place. But compared to my solution it should not rely on the controller to build the JSON response. This should be an concern of the view, however I could not find a way to define a view for a specific exception / status code.

How would you solve this problem?

module Datsu::Controllers::Identities
  class Create
    include Datsu::Action

    expose :identity

    params do
      param :email, presence: true
    end

    def call(params)
      halt 422, Datsu::Views::Errors::UnprocessableEntity.render(exposures) unless params.valid?

      @identity = {
        id: 1,
        email: 'david@strauss.io'
      }
    end
  end
end
module Datsu::Views::Errors
  class UnprocessableEntity
    include Datsu::View

    format :json

    def render
      mapped_errors = errors.to_h.inject({}) { |hash, (key, value)| hash.merge({ key.to_sym => value.map(&:validation) }) }

      JSON.generate({ errors: mapped_errors.to_h })
    end
  end
end

#2

@stravid Hello. I’d solve the issue in this way:

module Preconditions
  def self.included(action)
    action.class_eval do
      before :validate!
    end
  end

  private
  def validate!
    error(412) unless params.valid?
  end

  def error(code)
    # This is an example, replace it according to your needs
    message = JSON.generate(params.errors)

    halt code, message
  end
end

Datsu::Application.configure do
  controller.prepare do
    include Preconditions
  end
end

It will add this validation logic for ALL the actions in Datsu application.

When you don’t use use param validations, params.valid? always returns true. This logic is basically a no-op.
When you use validations, it may throw 412 via #error.

Lotus::Action#halt can be used with a valid HTTP code only. For instance halt 404, it will return a message "Not Found" by default. When used with two args, it allows to customize the body of the response. Example: halt 412, "Oh no!".

I’d not worry about having this logic in a view, the code presented here is faster. I see it as a result of a computation, not as presentational logic. If you’re still not convinced, I’d use a custom error serializer.


#3

Thank you very much for the response, this works nicely! And your argument why it’s not a concern of the view is compelling.

One more question, where in the directory structure would you store the module?


#4

A good candidate can be
apps/datsu/controllers/preconditions.rb

Remember to explicit do require_relative from apps/datsu/application.rb. :smile:


#5

@jodosha Another question came up regarding uniqueness validations. I’m handling them on the database level with an unique index. When this rule is violated an Sequel::UniqueConstraintViolation error is raised.

How would you handle this in an action? (Taking into consideration that the client should get an response with the errors.)

As far as I can tell handle_exception doesn’t work in this case since it only takes an HTTP status code. Then I tried to add the error to the errors but could not find an appropriate public API within lotus/validation. My current solution looks like the following, I’m hoping it can be DRY’ed up a little bit.

module Datsu::Controllers::Identities
  class Create
    include Datsu::Action

    expose :identity

    params do
      param :identity do
        param :email, presence: true
        param :password, presence: true
      end
    end

    def initialize(repository = IdentityRepository)
      @repository = repository
    end

    def call(params)
      @identity = Identity.new params[:identity]
      @repository.create @identity
    rescue Sequel::UniqueConstraintViolation
      halt_with_error 'identity.email', :uniqueness
    end
  end
end
module ParameterValidation
  private
  def validate!
    halt_with_errors(error_map) unless params.valid?
  end

  def error_map
    errors.to_h.inject({}) { |hash, (key, value)| hash.merge({ key.to_sym => value.map(&:validation) }) }.to_h
  end

  def halt_with_errors(errors)
    halt 422, JSON.generate({ errors: errors })
  end

  def halt_with_error(attribute, error)
    halt_with_errors({ attribute => [ error ] })
  end
end

What is a proper way to handle a uniqueness constraint?
#6

I suggest to use Lotus::Interactor. Now your action (HTTP abstraction) is concerned with database details.

class CreateIdentity
  include Lotus::Interactor

   def initialize(params)
     @identity = Identity.new(params[:identity])
   end

    def call
      persist_identity
    end

    private
    def persist_identity
      @identity = IdentityRepository.create(@identity)
    rescue Sequel::UniqueConstraintViolation
      error! "Your error message here"
    end
end

module Datsu::Controllers::Identities
  class Create
    include Datsu::Action

    def initialize(interactor = CreateIdentity)
      @interactor = interactor
    end

    def call(params)
      result = interactor.new(params).call

      if result.success?
        # ...
      else
         result.error
      end
    end
  end
end

#7

Thanks for the input, broadly spoken this means I should treat parameter validation and violations of the unique constraint as two separate things, correct?

Is there a specific reason the IdentityRepository is used without dependency injection in the interactor?


#8

I don’t see the reason. My personal rule for testing interactors is to mock nothing. I want to hit the database and use real collaborators.


#9