Search code examples
crystal-langkemal

Return Granite validation errors as JSON


I have a Granite User model with some validations. When someone makes a POST request to users/new, I'd like to return the validation errors (if any) as JSON. Currently, I have:

if user.errors.size > 0
  halt env, status_code: 500, response: user.errors.to_json
end

But when I try to compile, I get:

in /usr/local/Cellar/crystal/0.26.1/src/json/to_json.cr:66: no overload 
matches 'Granite::Error#to_json' with type JSON::Builder
Overloads are:
- Object#to_json(io : IO)
- Object#to_json()

  each &.to_json(json)
         ^~~~~~~

Solution

  • So the issue is that User#errors is an Array(Granite::Error), i.e. an Array holding Granite::Errors. Unfortunately, it doesn't look like Granite::Error implements the to_json(JSON::Builder) method (i.e. the method to_json taking a parameter of type JSON::Builder), which Array#to_json relies on (that snippet you see there is from the implementation of Array#to_json which you can view on GitHub.).

    I'd recommend building the JSON yourself using JSON.build. This has the added side-effect of keeping the JSON that you respond with (which I imagine is being consumed by some client) is entirely in your control. If the developers of Granite were to change how they encode Granite::Errors in JSON and you were using their to_json method, the change wouldn't raise anything at compile time.

    As a side-note, I'd recommend against using a status code of 500 to denote a validation error, as that is typically reserved for unexpected errors internal to your server. A 4xx error (e.g. 400 - Bad Request) would be more appropriate. As a second side-note, it would be more RESTful to have the POST be made to the /users endpoint, as opposed to /users/new.

    With these changes, here would be the resulting snippet:

    if user.errors.size > 0
      errors_as_json = JSON.build do |json|
        json.array do
          user.errors.each do |error|
            json.string error.to_s
          end
        end
      end
      halt env, status_code: 400, response: errors_as_json
    end