Json Snake Case

Jorge working on his laptop. In the speech bubble appears a JSON fragment with fields in snake-case and their value in camel-case.

Table of contents


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 🏃‍♂️