Rails JSON API parameter validation & error responses

10,909

Solution 1

I think you are confusing the use of strong_parameters with validation methods.

Strong parameters provides an interface for protecting attributes from end-user assignment. This makes Action Controller parameters forbidden to be used in Active Model mass assignment until they have been whitelisted.

http://edgeapi.rubyonrails.org/classes/ActionController/StrongParameters.html

If you whitelist parameters in strong-parameters, it means you will be able to mass-assign them to your ActiveRecord model. Or better, attributes which are not whitelisted will not get passed to your model.

They actually have nothing to do with validations itself. They are meant to protect your model for unpermitted parameter injection.

def create
  @user = User.new(create_user_params)
end

def create_user_params
  params.require(:user).permit(:email, :phone)
end

so your example:

POST /users body={user: {email: '[email protected]', phone: '1234', some_other: 'some other'}}

the some_other attribute will not get passed to your User.new

What you are looking for IS activerecord validations. In your specific case, I would write something like:

def create
  @user = User.new(create_user_params)
  if @user.save
    render json: @user
  else
    render @user.errors.full_messages.as_json, status: 400
  end
end

def create_user_params
  params.require(:user).permit(:email, :phone)
end

and then in your model:

class User < ActiveRecord::Base
  validates_precense_of :some_other #this will cause the user not to save, end thus, report an errer message
end

Solution 2

The "Rails way" supposed to use a form roundtrip - all validations should live in a model. Controllers might have only simple validation like security and whitelist, not related to business logic. "Model" might be not only ActiveRecord itself, but also something like FormObject https://robots.thoughtbot.com/activemodel-form-objects especially if some of your resources are not reflected 1-to-1 to a database tables.

If you are going to use only API mode, I would recommend to give a chance for very neat API DSL https://github.com/ruby-grape/grape#parameter-validation-and-coercion

Share:
10,909
CaptainStiggz
Author by

CaptainStiggz

Updated on June 11, 2022

Comments

  • CaptainStiggz
    CaptainStiggz almost 2 years

    I'm new to Rails and have a couple questions about validating parameters and returning error responses. I want to create a JSON API using the new Rails 5 API mode.

    As far as I can tell, Rails recommends using "strong parameters" as a baseline for validating parameters. If I want to, for example, create a User class that requires either a phone number or email, I start with something like this in my UsersController.

    def create
      @user = User.new(create_user_params)
    end
    
    def create_user_params
      params.require(:user).permit(:email, :phone)
    end
    

    Now if I want something a little more complex, I might add the following

    def create
      arr_contains_at_least_one(params[:user], [:email, :phone])
      @user = User.new(create_user_params)
    end
    

    This brings us to my question.

    What's the best way to return a "pretty" error response, both in the event of a default Rails error (ActionController::ParameterMissing) or a custom error? By pretty I mean, if I called the API endpoint in a browser, it'd return readable JSON with a descriptive message. If I run my server in production mode, and fail to provide a user parameter in the first example, rails returns:

    An unhandled lowlevel error occurred. The application logs may have details.
    

    This is obviously no good, especially if I wished to display the error to an end user. Here I assume the strong parameters pattern is for security and data integrity, not user experience. It would seem the same is true of model field validation. So I make the following adjustments.

    def create
      return error_response("some error") unless arr_contains_at_least_one(params[:user], [:email, :phone])
      @user = User.new(create_user_params)
    end
    
    def error_response(msg, status = 400)
      render json: {"code":status, "message": msg}, :status => status 
    end
    

    This works, but now I'm forced to manually write parameter checks (to check for presence of mandatory parameters, and to validate parameters such as email addresses) and their corresponding error responses. If I'm using Rails's built-in field validation on models as well, this seems to violate the DRY principle. Further, designing a good error handling pattern requires quite a bit of custom implementation.

    Am I missing out on some Rails magic or am I on the right track here?

    EDIT: It looks like active record validations can be fairly easily wrapped in a JSON response (http://guides.rubyonrails.org/active_record_validations.html) but the question still remains for validating presence of parameters.