Codementor Events

Best way to Validate HTTP parameters in Rails

Published Dec 22, 2023
Best way to Validate HTTP parameters in Rails

Rails has strong params for validating HTTP parameters to ascertain if the request is well formed. I had a case where an API endpoint was meant to receive somewhat intricate data structure. While strong params could do it, there were problems that arises namely:

  1. While Strong params checks the existence of data, it does not do more strict validations to verify the type of each parameter.
  2. Stong params could get messy and become less developer friendly.
  3. Scalability issues arises.

For example let's define a declaration to permits the name, age, contacts, and associates attributes. It is expected that contacts will consist of allowed individual values, associates will comprise an array of resources possessing defined attributes. They must include a name attribute with allowance for any permitted values, a skills attribute as an array of permitted individual values, and a team attribute which is restricted to having a name allowed.
Here is what strong params looks like in this case:

params.permit(:name, :age, { contacts: [] },
              associates: [ :name,
              { team: [ :name ], skills: [] }])

Based on the issues we outlined earlier, we have the opportunity to improve the clarity of this by replacing strong params with The dry-schema gem.
The dry-schema gem empowers us to articulate specific schemas that our data must adhere to. Similar to strong parameters, it automatically excludes keys that are not explicitly specified in the schema. Here is a link to the documentation

ContactSchema = Dry::Schema.Params do
  required(:name).filled(:string)
  required(:age).filled(:integer)
  required(:contacts).value(array[:string]).value(min_size?: 1)
 required(:skills).array(:string)
  required(:associates).array(:hash) do
    required(:name).filled(:string)
    required(:team).hash do
      required(:name).filled(:string)
    end
  end
end

The syntax required(:associates).array(:hash) might seem a bit complex at first glance, but it essentially signifies an array of any length, where each element is a hash. The block within this method then outlines the allowed keys within those hashes. This an advantage we just gained by using dry-schema.

The following hash would pass the checks for this schema.

params = {
  name: "John Doe",
  age: 27,
  contacts: ["Mary Smith", "Michael Jordan"],
  skills: ["Ruby", "NodeJS"],
associates: [ {
name: "ITA",
  team:[
    {
      name: "Remote"
    }
  ]
}]
}

We can check this with:

parsed = ContactSchema.(params)
We will get a Dry::Schema::Result back from this, which we can grab the output of with:
parsed.output
Dry::Scema also provides beautiful error messages.
demonstrated in the example below:
params = {}
parsed = ContactSchema.(params)

The resulting error messages resemble those seen in Active Model validations:

=> {:name=>["is missing"], :age=>["is missing"], :contacts=>["is missing"], :associatess=>["is missing"], :skills=>["is missing"]}

Furthermore, parsed will respond to success? with false. This means we can utilize this feature in a controller action to verify the validity of parameters before forwarding them to their final destination. This destination could be a model, potentially with its own validations, or another service.

keep in mind that dry-schema automatically or implicitly convert values from one data type to another.
If age is specifed as a string "27" and the schema says it must be an integer, it gets converted to an integer.
Dry::Schema.Params type will make an effort to convert string parameter values into their corresponding Ruby equivalents. This functionality extends to formats like dates in "yyyy/mm/dd" eliminating the need for explicit Date.parse conversions when the parameter is directed to a service object instead of a model.

One of the key advantage is code re-use helping developers to adhere to the dry principle which is top-most requirement in rails.
Reusing schemas is a powerful feature in dry-schema. We can further refactor our example to use two schemas, ContactSchema and AssociateSchema, which define the structures for individuals and their associates. We can leverage them together like this:

AssociateSchema = Dry::Schema.params do
  required(:name).filled(:string)
  required(:team).hash do
    required(:name).filled(:string)
  end
end

ContactSchema = Dry::Schema.Params do
  required(:name).filled(:string)
  required(:age).filled(:integer)
  required(:emails).value(array[:string]).value(min_size?: 1)
  required(:associates).array(AssociateSchema)
  required(:hobbies).array(:string)
end

This is beautiful and juicy. I encourage you to venture into it if you haven't started using it yet to start deriving these benefits.

The documentation has several uses cases and capabilities we haven't even scratched. I encourage you to dive into it.
Here is a link to a sample use case

Discover and read more posts from Nsikan Sylvester
get started