Problem
When integrating REST APIs, it’s very common for JSON keys to come in snake_case (for example, first_name) while in Swift we model properties in camelCase (firstName).
If we don’t configure anything, we have to manually write CodingKeys in each model or accept decoding errors.
We’re looking for a centralized and reusable way to:
- Decode JSON without manual CodingKeys.
- Encode our models when sending data.
- Maintain the same behavior in both iOS/macOS apps (URLSession) and Vapor (req/res content).
Solution
We create extensions for JSONDecoder and JSONEncoder that expose convenience constructors and static snakeCase shortcuts.
Advantages:
- Zero boilerplate in models: avoids repetitive CodingKeys.
- Self-explanatory name when using them: JSONDecoder.snakeCase / JSONEncoder.snakeCase.
- Consistency across the entire project (client and server).
extension JSONDecoder {
convenience init(keyDecodingStrategy: KeyDecodingStrategy) {
self.init()
self.keyDecodingStrategy = keyDecodingStrategy
}
static var snakeCase: JSONDecoder {
.init(keyDecodingStrategy: .convertFromSnakeCase)
}
}
extension JSONEncoder {
convenience init(keyEncodingStrategy: KeyEncodingStrategy) {
self.init()
self.keyEncodingStrategy = keyEncodingStrategy
}
static var snakeCase: JSONEncoder {
.init(keyEncodingStrategy: .convertToSnakeCase)
}
}
Note: if a model needs a specific key name, you can still use CodingKeys locally; the snake_case strategy will act as the default value.
Result
Usage examples
// iOS / macOS: reading data
let data: Data = ...
let user = try JSONDecoder.snakeCase
.decode(UserDTO.self, from: data)
// iOS / macOS: sending data
let body = CreateUserDTO(firstName: "Ada", lastName: "Lovelace")
request.httpBody = try JSONEncoder.snakeCase.encode(body)
// Vapor
let input = try req.content
.decode(CreateUserDTO.self, using: JSONDecoder.snakeCase)
With these shortcuts we get:
- Fewer errors and greater readability: properties remain in idiomatic Swift camelCase.
- Immediate interoperability with legacy snake_case APIs.
- Single configuration reusable throughout the project (tests included).
This standardizes how we serialize/parse JSON without sacrificing clarity or fine-grained control when needed.
Keep coding, keep running 🏃♂️