A post, from JSON to Swift structs
With Post as the Root name, the sample becomes:
{
"postId": 42017,
"author": { "handle": "@lena", "verified": true },
"text": "First snow in Tromsø",
"media": [
{
"type": "photo",
"url": "https://cdn.example.com/p/42017-1.jpg",
"altText": "Snowy street at dusk"
}
],
"likes": 318,
"replies": 12,
"postedAt": "2026-11-04T07:41:00Z",
"location": null
}
import Foundation
struct Post: Codable {
let postId: Int
let author: Author
let text: String
let media: [Media]
let likes: Int
let replies: Int
let postedAt: String
let location: JSONValue?
}
struct Media: Codable {
let type: String
let url: String
let altText: String
}
struct Author: Codable {
let handle: String
let verified: Bool
}
Properties are let; make them var to edit models after decoding. Whole numbers are Int, decimals Double. location held only null, so it falls back to JSONValue?, a small enum appended to the output whenever a value could be any JSON type. It stores every number as a Double, so swap in a real type once you know it. Each ? marks a value that was null or missing somewhere, after array items are merged (rules on JSON to TypeScript). Explore large payloads first in the JSON viewer.
Decoding with JSONDecoder
import Foundation
func loadPost(from body: String) throws -> Post {
try JSONDecoder().decode(Post.self, from: Data(body.utf8))
}
With URLSession, pass the data from try await URLSession.shared.data(from: url) to the same call. Optionals are decoded with decodeIfPresent, so a missing key and null both give nil, and undeclared keys are ignored. To get a Date for postedAt, change its type and set dateDecodingStrategy to .iso8601. The structs are Codable, so JSONEncoder().encode(post) writes JSON with the original keys.
When CodingKeys appear
A CodingKeys enum is only written when a property name differs from its JSON key, as with these snake_case keys and the root name Track:
{ "track_id": 812, "title": "Northern Lights", "duration_ms": 214000, "explicit": false }
import Foundation
struct Track: Codable {
let trackId: Int
let title: String
let durationMs: Int
let explicit: Bool
enum CodingKeys: String, CodingKey {
case trackId = "track_id"
case title
case durationMs = "duration_ms"
case explicit
}
}
title and explicit are listed too: once a type declares CodingKeys, every stored property without a default must be in it. A Swift keyword such as default keeps its name in backticks and needs no mapping. Don’t add keyDecodingStrategy = .convertFromSnakeCase on top: it turns track_id into trackId before matching, and then no key named track_id exists. For Android, JSON to Kotlin writes matching data classes.