cat swift/json-realistico-dati-non-collaborano.md
JSON realistico: quando i dati non collaborano
Il JSON dei tutorial è pulito; quello reale contiene chiavi mancanti, null, date strane, tipi incoerenti e campi inutili.
Il networking reale raramente assomiglia agli esempi perfetti. Nei tutorial trovi JSON pulito:
{
"id": "p-1",
"name": "Manuale Swift",
"price_cents": 2900
}
Nel mondo reale trovi:
{
"id": 123,
"name": "Manuale Swift",
"price_cents": "2900",
"category": null,
"created_at": "2026-05-04T08:00:00Z",
"metadata": {
"tracking": ""
}
}
Il corso deve insegnare a non confondere la forma della API con il dominio interno.
Problemi comuni
Nei JSON reali puoi incontrare:
- chiavi mancanti;
- valori
null; - numeri rappresentati come stringhe;
- date in formati diversi;
- snake_case;
- campi deprecati;
- array vuoti o assenti;
- oggetti annidati inutilmente;
- errori con struttura diversa dalle risposte di successo.
Codable è potente, ma non deve diventare un posto dove nascondere tutto.
Modello ingenuo
struct Prodotto: Decodable {
let id: String
let name: String
let price_cents: Int
let category: String
}
Problemi:
- nomi non Swift;
categorypuò esserenull;idpotrebbe arrivare come numero;price_centspotrebbe arrivare come stringa;- questo è un modello API, non un modello dominio.
DTO
Meglio creare un DTO:
struct ProductDTO: Decodable {
let id: String
let name: String
let priceCents: Int
let category: String?
enum CodingKeys: String, CodingKey {
case id
case name
case priceCents = "price_cents"
case category
}
}
Poi converti al dominio.
Il DTO rappresenta la risposta esterna. Il dominio rappresenta ciò che la tua app considera valido.
Decodifica tollerante
Se la API manda a volte numero e a volte stringa, puoi isolare la tolleranza nel DTO.
struct ProductDTO: Decodable {
let id: String
let name: String
let priceCents: Int
enum CodingKeys: String, CodingKey {
case id
case name
case priceCents = "price_cents"
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
if let stringID = try? container.decode(String.self, forKey: .id) {
id = stringID
} else {
let numericID = try container.decode(Int.self, forKey: .id)
id = String(numericID)
}
name = try container.decode(String.self, forKey: .name)
if let cents = try? container.decode(Int.self, forKey: .priceCents) {
priceCents = cents
} else {
let stringCents = try container.decode(String.self, forKey: .priceCents)
guard let cents = Int(stringCents) else {
throw DecodingError.dataCorruptedError(
forKey: .priceCents,
in: container,
debugDescription: "price_cents must be an integer or numeric string"
)
}
priceCents = cents
}
}
}
Questo codice non è bello, ma è nel posto giusto: al confine con il mondo esterno. Il dominio non deve sapere che price_cents a volte arriva come stringa.
Tollerare o fallire?
Non ogni campo mancante deve far fallire il decoding.
Se manca description, magari puoi usare nil.
Se manca id, probabilmente la risposta è inutilizzabile.
Domanda guida:
Questo campo è essenziale per creare un valore di dominio valido?
Valori opzionali
struct ProductDTO: Decodable {
let id: String
let name: String
let description: String?
}
Un optional nel DTO significa: la API potrebbe non mandarlo.
Un optional nel dominio significa: l’assenza ha senso per la logica interna.
Non sono sempre la stessa cosa.
Campi sconosciuti
Decodable ignora di default le chiavi che non hai modellato.
Questo è utile: non sei costretto a rappresentare ogni campo della API.
Modella solo ciò che ti serve.
Date realistiche
Le date sono uno dei punti più fragili.
let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .iso8601
Funziona solo se il formato è davvero ISO 8601 compatibile. Se la API usa formati misti, crea una strategia esplicita o decodifica la data come stringa nel DTO e convertila nel mapping.
struct ProductDTO: Decodable {
let createdAt: String
}
Poi:
extension ProductDTO {
func createdDate(using formatter: ISO8601DateFormatter) throws -> Date {
guard let date = formatter.date(from: createdAt) else {
throw ProductMappingError.invalidDate
}
return date
}
}
Non lasciare che una data sporca contamini tutto il dominio.
Error payload diverso
Molte API rispondono così in caso di successo:
{ "id": "p-1", "name": "Manuale Swift" }
E così in caso di errore:
{ "error": { "code": "not_found", "message": "Product not found" } }
Non provare a decodificare sempre lo stesso DTO. Prima controlla status code, poi scegli quale payload decodificare.
Checklist
Davanti a un JSON reale:
- quali campi sono obbligatori?
- quali possono mancare?
- quali sono
null? - quali formati data esistono?
- i tipi sono coerenti?
- il DTO coincide davvero col dominio?
- cosa facciò con campi extra?
- quale errore voglio produrre se il mapping fallisce?
Esercizio
Prendi un JSON prodotto con:
id;name;price_cents;category;created_at;description.
Definisci:
ProductDTO;Product;- una funzione
toDomain() throws -> Product.
Decidi quali campi sono obbligatori e quali opzionali.