cat swift/json-realistico-dati-non-collaborano.md

Lezione 4840 min

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;
  • category può essere null;
  • id potrebbe arrivare come numero;
  • price_cents potrebbe 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.