Background
In a previous article, we talked about moving development forward with OpenAPI as the foundation.
- Schema-first API development with committee × OpenAPI × Rails
- A rich, type-safe client life with auto-generated code, powered by OpenAPI Generator + TypeScript
During the frontend integration phase, we received a request: "Could you make the JSON keys in requests and responses camelCase?"
To explain briefly, the example above is snake_case, which is commonly used for variable names in Ruby. The example below, on the other hand, is called camelCase, which is commonly used in JavaScript and similar languages.
{"first_name": "foo", "last_name": "bar", "note": "snake case"}
{"firstName": "foo", "lastName": "bar", "note": "camel case"}
*Naming conventions for variables aren't governed by any absolute rule, but there tend to be preferred patterns depending on the programming language.
When you build a straightforward API server in Rails that returns JSON, both requests and responses will basically use snake_case keys. However, the frontend wants to use camelCase keys, and converting between the two every time you send a request or parse a response is a hassle.
So we looked into how to handle JSON keys as camelCase in requests and responses in Rails.
What we did
With that in mind, we considered separate approaches for responses and requests, while keeping the following in mind:
- Use snake_case as much as possible within the Rails codebase
- Consolidate the conversion logic between camelCase and snake_case as much as possible
- Avoid any significant impact on performance
Response
It might make more sense to start with the request side, but handling the response turned out to be simpler, so we'll introduce that first. That said, this assumes you're using ActiveModelSerializers.
If you're using ActiveModelSerializer, you can easily change the key pattern of the response by specifying key_transform. Referring to the documentation below, in our case all we need to do is specify :camel_lower.
Request
Say you send a POST request with camelCase JSON keys, as in the earlier example.
{"firstName": "foo", "lastName": "bar", "note": "camel case"}
What should we do if we want this to end up as snake_case data when Rails processes it, as shown below?
{"first_name": "foo", "last_name": "bar", "note": "snake case"}
Ideally, we'd want the conversion to already be done by the time the controller handles the request parameters, so we looked into it and found a question with the same intent on Stack Overflow, along with an answer.
- Stack Overflow: What is the best way to convert all controller params from camelCase to snake_case in Rails?
Here's the code excerpted from that answer (assuming Rails version 6).
# File: config/initializers/json_param_key_transform.rb
# Transform JSON request param keys from JSON-conventional camelCase to
# Rails-conventional snake_case:
ActionDispatch::Request.parameter_parsers[:json] = lambda { |raw_post|
# Modified from action_dispatch/http/parameters.rb
data = ActiveSupport::JSON.decode(raw_post)
# Transform camelCase param keys to snake_case
if data.is_a?(Array)
data.map { |item| item.deep_transform_keys!(&:underscore) }
else
data.deep_transform_keys!(&:underscore)
end
# Return data
data.is_a?(Hash) ? data : { '_json': data }
}
When we tried this as written, we were able to receive request parameters where the JSON keys had been converted from camelCase to snake_case.
So what exactly is this code doing?
Modified from action_dispatch/http/parameters.rb
Using this comment as a clue, we checked the Rails source code.
- GitHub: https://github.com/rails/rails/blob/master/actionpack/lib/action_dispatch/http/parameters.rb
In it, DEFAULT_PARSERS is defined, which appears to define the method that parses JSON.
DEFAULT_PARSERS = {
Mime[:json].symbol => -> (raw_post) {
data = ActiveSupport::JSON.decode(raw_post)
data.is_a?(Hash) ? data : { _json: data }
}
}
In other words, we understood that the code introduced in that article overrides the JSON-parsing method using deep_transform_keys!(&:underscore), forcing the keys of the parsed parameters into snake_case.
Summary
By configuring requests and responses as described above, we were able to have Rails work with snake_case keys internally while still behaving, from the perspective of the API client, as if the keys were in the client's preferred case.
Naturally, this can be adjusted for conventions other than camelCase too, so it should be easy to switch depending on the situation.
Parameters other than JSON
Besides request body parameters, Rails also deals with path parameters and query parameters.
Path parameters are embedded in the URL and don't have keys to speak of, so there's no issue as long as they're defined in snake_case in the Rails routes.
Query parameters, on the other hand, have keys specified by the client, so the same problem can occur there — but they aren't converted by the configuration above.
# The client uses userId, but Rails wants to treat it as user_id
GET /notes?userId=1
The example above is a bit contrived, so it might be solvable by rethinking the path design to avoid query parameters altogether.
However, if query parameters do become necessary, we'd like to avoid forcing the client to partially use snake_case, or introducing a constraint where key names can only be a single word.
We're planning to keep looking into a good way to resolve this.
Addendum
Looking into it further, it seems there's an approach using before_action to convert the entire parameter set at once. This is closer to what I originally had in mind, and it seems like a clearer approach. (Untested)
We haven't decided which approach to take yet, so we'll keep exploring and figure it out as we go.