Json Snake Case

Jorge trabajando en su portátil. En el bocadillo aparece un fragmento JSON con campos en snake-case y su valor en camel-case.

Tabla de contenidos


Problema

Cuando integramos APIs REST, es muy común que las claves JSON vengan en snake_case (por ejemplo, first_name) mientras que en Swift modelamos las propiedades en camelCase (firstName).
Si no configuramos nada, nos toca escribir CodingKeys a mano en cada modelo o aceptar errores de decodificación.

Buscamos una forma centralizada y reutilizable de:

  • Decodificar JSON sin CodingKeys manuales.
  • Codificar nuestros modelos al enviar datos.
  • Mantener el mismo comportamiento tanto en apps iOS/macOS (URLSession) como en Vapor (req/res content).

Solución

Creamos extensiones de JSONDecoder y JSONEncoder que exponen constructores de conveniencia y atajos estáticos snakeCase.
Ventajas:

  • Cero boilerplate en los modelos: evita CodingKeys repetitivas.
  • Nombre autoexplicativo al usarlas: JSONDecoder.snakeCase / JSONEncoder.snakeCase.
  • Consistencia en todo el proyecto (cliente y servidor).
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)
    }
}

Nota: si un modelo necesita un nombre de clave específico, puedes seguir usando CodingKeys localmente; la estrategia snake_case actuará como valor por defecto.


Resultado

Ejemplos de uso

// iOS / macOS: leer datos
let data: Data = ...
let user = try JSONDecoder.snakeCase
    .decode(UserDTO.self, from: data)

// iOS / macOS: enviar datos
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)

Con estos atajos obtenemos:

  • Menos errores y mayor legibilidad: las propiedades permanecen en camelCase idiomático Swift.
  • Interoperabilidad inmediata con APIs legacy en snake_case.
  • Configuración única y reutilizable en todo el proyecto (tests incluidos).

Esto estandariza cómo serializamos/parseamos JSON sin sacrificar claridad ni control fino cuando hace falta.

Keep coding, keep running 🏃‍♂️