Codable models without the boilerplate
Decoding JSON in Swift means declaring a type for every object, getting each property’s type and optionality right, and adding a CodingKeys enum whenever the API’s key names differ from Swift’s. This generator reads a sample payload, merges what every object at the same position looks like, and writes the models in one pass. Nested objects get their own types named after their keys, and the items of an array are named in the singular, so "categories": [...] produces [Category].
The type inference is shared with the other generators on this site, so a sample produces the same structure here as in the Kotlin, TypeScript or Rust output.
How JSON maps to Swift
- Objects become
structtypes conforming toCodable, with one stored property per key. - A key missing from some objects, or
nullin some of them, becomes an optional such asString?;JSONDecoderfills it withnileither way. - Whole numbers become
Int, which is 64-bit on every current Apple platform and on Linux. Other numbers becomeDouble. - An integer outside the 64-bit range, or a decimal with more digits than a
Doublekeeps (for example a price written to 20 places), becomesDecimalinstead, whichJSONDecoderreads digit by digit. Beyond 38 significant digits evenDecimalrounds, and a warning says so. - Arrays become
[Element]and objects used as dictionaries become[String: Value]. - A position holding different JSON types across the sample, such as
1in one object and"one"in another, becomes anenumwith one case per type and a hand-writteninit(from:)that tries the most specific type first, sotrueis never read as a number. - A position that is only ever
null, or an array that is always empty, gives no type information. It uses a smallJSONValueenum that the output includes, which decodes any JSON value. - A type that contains itself without an array in between, like a linked-list
nextfield, is emitted as afinal class, because a struct cannot contain itself.
Options
Root type name names the top-level type; it is converted to PascalCase, and a JSON array or scalar at the root becomes a typealias with that name.
Declare as chooses struct (value semantics, the usual choice for decoded data) or final class (reference semantics, useful when models are shared and mutated across views).
var properties switches the stored properties from let to var so decoded values can be edited in place.
camelCase property names (on by default) turns user_id into userId and first-name into firstName. Whenever a property name differs from its key, a CodingKeys enum lists every property with its original key, so the mapping is explicit and you do not need keyDecodingStrategy. With the option off, keys that are already valid Swift identifiers are kept as they are and only invalid characters are replaced. Swift keywords such as default are escaped with backticks.
ISO 8601 date-time strings as Date types a string as Date when every value at that position is a full timestamp with a time zone, such as 2024-03-11T10:00:00Z. The comment at the top shows the decoder setup: .iso8601, or a provided .iso8601WithFractionalSeconds strategy when some timestamps have fractions, which the built-in strategy rejects. Date-only strings like 2024-03-11 stay String.
Using the output
The comment at the top shows the decoding call. Add Equatable or Hashable to the conformance list if you compare models or use them in SwiftUI lists, and tighten any type the sample could not pin down. Unknown keys in later responses are ignored by JSONDecoder, so extra fields never break decoding, but a key the models require that is missing does throw: make it optional if the API may omit it. Run the result through the Swift formatter after editing.
Examples
snake_case API response
CodingKeys map user_id to userId; verified is optional because one follower lacks it, and the always-null avatar_url uses JSONValue?.
{
"user_id": 42,
"display_name": "Aisha Tan",
"avatar_url": null,
"followers": [
{ "user_id": 7, "display_name": "Ben" },
{ "user_id": 9, "display_name": "Chloé", "verified": true }
]
}// Decode with JSONDecoder:
// let value = try JSONDecoder().decode(Profile.self, from: data)
import Foundation
struct Profile: Codable {
let userId: Int
let displayName: String
let avatarUrl: JSONValue?
let followers: [Follower]
enum CodingKeys: String, CodingKey {
case userId = "user_id"
case displayName = "display_name"
case avatarUrl = "avatar_url"
case followers
}
}
struct Follower: Codable {
let userId: Int
let displayName: String
let verified: Bool?
enum CodingKeys: String, CodingKey {
case userId = "user_id"
case displayName = "display_name"
case verified
}
}
/// Any JSON value, for positions where the sample shows no type (only null, or empty arrays).
enum JSONValue: Codable, Hashable {
case null
case bool(Bool)
case number(Double)
case string(String)
case array([JSONValue])
case object([String: JSONValue])
init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
if container.decodeNil() {
self = .null
} else if let value = try? container.decode(Bool.self) {
self = .bool(value)
} else if let value = try? container.decode(Double.self) {
self = .number(value)
} else if let value = try? container.decode(String.self) {
self = .string(value)
} else if let value = try? container.decode([JSONValue].self) {
self = .array(value)
} else {
self = .object(try container.decode([String: JSONValue].self))
}
}
func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
switch self {
case .null:
try container.encodeNil()
case .bool(let value):
try container.encode(value)
case .number(let value):
try container.encode(value)
case .string(let value):
try container.encode(value)
case .array(let value):
try container.encode(value)
case .object(let value):
try container.encode(value)
}
}
}
Timestamps as Date, mutable class
Both timestamps become Date and the fractional seconds add a flexible decoding strategy; the date-only field stays a String.
{
"id": "evt_1",
"startsAt": "2024-03-11T09:30:00Z",
"updatedAt": "2024-03-11T10:00:00.250+08:00",
"day": "2024-03-11"
}// Decode with JSONDecoder:
// let decoder = JSONDecoder()
// decoder.dateDecodingStrategy = .iso8601WithFractionalSeconds
// let value = try decoder.decode(Event.self, from: data)
// Encode dates back as ISO 8601 with encoder.dateEncodingStrategy = .iso8601.
import Foundation
final class Event: Codable {
var id: String
var startsAt: Date
var updatedAt: Date
var day: String
}
extension JSONDecoder.DateDecodingStrategy {
/// ISO 8601 date-times with or without fractional seconds (.iso8601 rejects fractions).
static var iso8601WithFractionalSeconds: Self {
.custom { decoder in
let container = try decoder.singleValueContainer()
let text = try container.decode(String.self)
let formatter = ISO8601DateFormatter()
formatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
if let date = formatter.date(from: text) {
return date
}
formatter.formatOptions = [.withInternetDateTime]
if let date = formatter.date(from: text) {
return date
}
throw DecodingError.dataCorruptedError(in: container, debugDescription: "Expected an ISO 8601 date-time, got \(text)")
}
}
}
Large numbers and mixed values
The ID beyond Int64 and the over-precise amount become Decimal; the mixed array becomes [Value?], an enum with double and string cases.
{
"snowflake": 12345678901234567890,
"amount": 19.999999999999999999,
"count": 3,
"values": [1, "two", 3.5, null]
}// Decode with JSONDecoder:
// let value = try JSONDecoder().decode(Root.self, from: data)
import Foundation
struct Root: Codable {
let snowflake: Decimal
let amount: Decimal
let count: Int
let values: [Value?]
}
enum Value: Codable {
case double(Double)
case string(String)
init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
if let value = try? container.decode(Double.self) {
self = .double(value)
return
}
if let value = try? container.decode(String.self) {
self = .string(value)
return
}
throw DecodingError.typeMismatch(Value.self, DecodingError.Context(codingPath: decoder.codingPath, debugDescription: "Expected Double or String"))
}
func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
switch self {
case .double(let value):
try container.encode(value)
case .string(let value):
try container.encode(value)
}
}
}
Common errors and how to fix them
| Error | Cause | Fix |
|---|---|---|
Trailing comma before '}'Explained | The sample is not valid JSON, often because of a trailing comma copied from JavaScript. | Delete the comma after the last member; the JSON formatter can also fix it for you. |
Some numbers have more than 38 significant digits; Swift's Decimal keeps 38, so those values lose precision. | A warning: a number in the sample is longer than Decimal can represent. | Ask the API to send such values as strings and type the property as String. |
keyNotFound(CodingKeys(stringValue: "…")) at runtime | A key that was present in every sample object is missing from a real response, so its property was generated as non-optional. | Add more varied objects to the sample, or make that property optional by hand. |
typeMismatch(Swift.Int, …) at runtime | The sample only had whole numbers at a position where the API sometimes sends decimals. | Change the property to Double or Decimal, or include a decimal value in the sample. |
Frequently asked questions
Do I need a third-party library?
No. The models use only Codable and Foundation, so they work with JSONDecoder on iOS, macOS, watchOS, tvOS and Linux.
Why CodingKeys instead of convertFromSnakeCase?
Explicit keys work for any naming style, including kebab-case and keys with spaces, and they keep each model correct regardless of how the decoder is configured.
How are Int and Double chosen?
By how the numbers are written: a position with only integer literals becomes Int, and any decimal or exponent at that position makes it Double.
Is my JSON uploaded?
No. Inference and code generation both run locally in this browser tab.
Can it generate Equatable or Hashable conformances?
Add them to the generated declarations yourself: Swift synthesises both automatically when every property type supports them, which all generated types do.