Convert ActiveRecord Validation Errors to API Consumption Errors

I am writing a fairly standard CRUD RESTful API in Rails 4. However, I do not agree with error handling.

Imagine I have the following model:

class Book < ActiveRecord::Base
  validates :title, presence: true
end

If I try to create a book object without a title, I get the following error:

{
  "title": [
    "can't be blank"
  ]
}

ActiveRecord validations are intended for use with forms. Ideally, I would like to map each readable validation error to a constant that can be used by an API consumer. So something like:

{
  "title": [
    "can't be blank"
  ],
  "error_code": "TITLE_ERROR"
}

This can be used to display errors associated with a person ("the title cannot be empty") and can be used in other code ( if response.error_code === TITLE_ERROR...). Are there any tools for this in Rails?

EDIT: Rails 2 .

+4
5

error_codes.yml API, status_code, title, details code, API.

:

api:
  invalid_resource:
    code: '1'
    status: '400'
    title: 'Bad Request'

not_found:
    code: '2'
    status: '404'
    title: 'Not Found'
    details: 'Resource not found.'

config/initializers/api_errors.rb YAML .

API_ERRORS = YAML.load_file(Rails.root.join('doc','error-codes.yml'))['api']

///error_handling.rb API JSON:

module ErrorHandling
  def respond_with_error(error, invalid_resource = nil)
    error = API_ERRORS[error]
    error['details'] = invalid_resource.errors.full_messages if invalid_resource
    render json: error, status: error['status']
  end
end

API , , :

include ErrorHandling

:

respond_with_error('not_found') # For standard API errors
respond_with_error('invalid_resource', @user) # For invalid resources

, :

def create
  if @user.save(your_api_params)
    # Do whatever your API needs to do
  else
    respond_with_error('invalid_resource', @user)
  end
end

, API, :

# For invalid resources
{
  "code": "1",
  "status": "400",
  "title": "Bad Request",
  "details": [
    "Email format is incorrect"
  ]
}

# For standard API errors
{
  "code": "2",
  "status": "404",
  "title": "Not Found",
  "details": "Route not found."
}

API YAML , API.

+6

:

def create
  book = Book.new(book_params)
  if user.save
    render json: book, status: 201
  else
    render json: { errors: book.errors, error_code: "TITLE_ERROR" }, status: 422
  end
end

json, , , , "title" "error_code" "". , .

+1

: (, ), .

, , , @baron816, , - .

:

1 -. , custom_error_codes, , , Array ( ).

module ErrorCodesConcern
  extend ActiveSupport::Concern

  included do
    # storage for the error codes
    attr_reader :custom_error_codes
    # reset error codes storage when validation process starts
    before_validation :clear_error_codes
  end

  # default value so the variable is not empty when accessed improperly 
  def custom_error_codes
    @custom_error_codes ||= []
  end

  private 
  def clear_error_codes
    @custom_error_codes = []
  end
end

class MyModel < ActiveRecord::Base
  include ErrorCodesConcern
  ...
end

2 - , . , (activemodel-gem-path)/lib/active_model/validations/.

,

class CustomPresenceValidator < ActiveModel::Validations::PresenceValidator
  # this method is copied from the original validator
  def validate_each(record, attr_name, value)
    if value.blank?
      record.errors.add(attr_name, :blank, options) 
      # Those lines are our customization where we add the error code to the model
      error_code = "#{attr_name.upcase}_ERROR"
      record.custom_error_codes << error_code unless record.custom_error_codes.include? error_code
    end
  end
end

class Book < ActiveRecord::Base
  validates :title, custom_presence: true
end

3 - , , , (. @baron816) custom_error_codes.

+1

, .

Book , .

, , ,

1 -

ApplicationController

# Handle validation errors
rescue_from ActiveRecord::RecordInvalid do |exception|
  messages = exception.record.errors.messages
  messages[:error_codes] = messages.map {|k,v| k.to_s.upcase << "_ERROR" }
  render json: messages, status: 422
end

note that error_codesin this case is an array that allows multiple error codes. eg:

{
  "title": [
    "can't be blank"
  ],
  "author": [
    "can't be blank"
  ],
  "error_codes": ["TITLE_ERROR", "AUTHOR_ERROR"]
}

Solution 2 - Handling only the first validation error

If you really want to save only one validation error, use this instead

# Handle validation errors
rescue_from ActiveRecord::RecordInvalid do |exception|
  key = exception.record.errors.messages.keys[0]
  msg = exception.record.errors.messages[key]
  render json: { key => msg, :error_code => key.to_s.upcase << "_ERROR" }, status: 422
end

which will give you an answer like

{
  "title": [
    "can't be blank"
  ],
  "error_code": "TITLE_ERROR"
}

even if you have a few mistakes

+1
source

Try the following:

book = Book.new(book_params)
if user.save
  render json: book, status: 201
else
  render json: { 
           errors: book.errors,
           error_codes: book.errors.keys.map { |f| f.upcase + "_ERROR" }
         },
         status: 422
end

error_codes will return several error codes.

+1
source

Source: https://habr.com/ru/post/1658872/


All Articles