Python e dizionari: validare e trasformare dati ricevuti da un’API

by theArchitect
SHARE
Python e dizionari: validare e trasformare dati ricevuti da un’API
© Guida-HTML5.it

Introduzione

Quando un programma Python comunica con un’API, riceve spesso dati strutturati sotto forma di dizionari annidati. Un esempio tipico è una risposta JSON che contiene informazioni su un cliente, un ordine o una prenotazione. Anche se il formato JSON è ordinato e leggibile, i dati ricevuti potrebbero essere incompleti, avere tipi errati oppure contenere campi non necessari.

I dizionari sono particolarmente adatti per questo scenario perché permettono di accedere rapidamente ai valori tramite chiave, controllare la presenza dei campi e costruire una nuova struttura più semplice da usare nel resto dell’applicazione.

In questo tutorial realizzeremo una funzione che:

  • valida i campi obbligatori di una richiesta;
  • controlla il tipo e il contenuto dei valori;
  • ignora eventuali campi sconosciuti;
  • trasforma i dati in un formato interno coerente;
  • restituisce errori comprensibili senza interrompere il programma.

Questo approccio è utile in applicazioni web, servizi REST, script di importazione e programmi che leggono dati da file JSON.

Codice completo

from datetime import datetime


def valida_e_normalizza_ordine(dati):
    """
    Valida un dizionario ricevuto dall´esterno e restituisce:
    - un dizionario normalizzato;
    - una lista di errori.
    """

    errori = []

    # Controllo del tipo principale
    if not isinstance(dati, dict):
        return None, ["Il payload deve essere un dizionario"]

    # Campi obbligatori
    campi_obbligatori = ["id_ordine", "email_cliente", "totale", "articoli"]

    for campo in campi_obbligatori:
        if campo not in dati:
            errori.append(f"Campo obbligatorio mancante: {campo}")

    # Se mancano campi fondamentali, non è possibile continuare
    if errori:
        return None, errori

    # Validazione dell´identificativo
    id_ordine = dati["id_ordine"]

    if not isinstance(id_ordine, int) or id_ordine <= 0:
        errori.append("id_ordine deve essere un intero positivo")

    # Validazione dell´email
    email = dati["email_cliente"]

    if not isinstance(email, str) or "@" not in email:
        errori.append("email_cliente non è valida")
    else:
        email = email.strip().lower()

    # Validazione del totale
    totale = dati["totale"]

    if not isinstance(totale, (int, float)) or totale < 0:
        errori.append("totale deve essere un numero maggiore o uguale a zero")
    else:
        totale = round(float(totale), 2)

    # Validazione degli articoli
    articoli_ricevuti = dati["articoli"]
    articoli_normalizzati = []

    if not isinstance(articoli_ricevuti, list):
        errori.append("articoli deve essere una lista")
    else:
        for posizione, articolo in enumerate(articoli_ricevuti, start=1):
            if not isinstance(articolo, dict):
                errori.append(
                    f"L´articolo {posizione} deve essere un dizionario"
                )
                continue

            if "codice" not in articolo:
                errori.append(
                    f"L´articolo {posizione} non contiene il codice"
                )
                continue

            quantita = articolo.get("quantita", 1)

            if not isinstance(quantita, int) or quantita <= 0:
                errori.append(
                    f"La quantità dell´articolo {posizione} non è valida"
                )
                continue

            articoli_normalizzati.append({
                "codice": str(articolo["codice"]).strip().upper(),
                "quantita": quantita
            })

    if errori:
        return None, errori

    # Costruiamo un nuovo dizionario controllato.
    # I campi extra ricevuti non vengono copiati.
    ordine_normalizzato = {
        "id": id_ordine,
        "cliente": email,
        "totale_euro": totale,
        "articoli": articoli_normalizzati,
        "ricevuto_il": datetime.now().isoformat(timespec="seconds")
    }

    return ordine_normalizzato, []


# Esempio di payload ricevuto da un´API
payload = {
    "id_ordine": 1042,
    "email_cliente": "  [email protected] ",
    "totale": 89.9,
    "articoli": [
        {"codice": " ab-12 ", "quantita": 2},
        {"codice": "xy-99"},
    ],
    "campo_non_previsto": "valore da ignorare"
}

ordine, errori = valida_e_normalizza_ordine(payload)

if errori:
    print("Dati non validi:")
    for errore in errori:
        print(f"- {errore}")
else:
    print("Ordine accettato:")
    print(ordine)


# Esempio con dati errati
payload_non_valido = {
    "id_ordine": -5,
    "email_cliente": "indirizzo-senza-chiocciola",
    "totale": -10,
    "articoli": "nessuna lista"
}

ordine, errori = valida_e_normalizza_ordine(payload_non_valido)

if errori:
    print("
Errori del secondo ordine:")
    for errore in errori:
        print(f"- {errore}")

Spiegazione

Controllare la presenza delle chiavi

Per verificare se una chiave esiste in un dizionario si usa l’operatore in:

if "email_cliente" not in dati:
    print("Email mancante")

Questo controllo è preferibile rispetto all’accesso diretto quando il campo potrebbe non esistere. Infatti, dati["email_cliente"] genera un’eccezione KeyError in caso di chiave assente.

Usare get() per valori opzionali

Il metodo get() consente di specificare un valore predefinito:

quantita = articolo.get("quantita", 1)

Se la chiave quantita esiste, viene restituito il suo valore; altrimenti viene usato 1. Questa tecnica è utile per i campi facoltativi.

Validare prima di trasformare

Il codice non modifica direttamente il dizionario originale. Prima verifica i dati e poi costruisce ordine_normalizzato. Questa scelta evita di lasciare l’oggetto in uno stato parzialmente modificato se viene rilevato un errore.

La normalizzazione applica inoltre alcune regole pratiche:

  • rimuove gli spazi dall’indirizzo email;
  • converte l’email in lettere minuscole;
  • arrotonda il totale a due cifre decimali;
  • converte i codici articolo in maiuscolo;
  • mantiene soltanto i campi necessari all’applicazione.

Dizionari annidati e liste

Il campo articoli contiene una lista di dizionari. Per elaborarla si usa un ciclo for:

for articolo in dati["articoli"]:
    codice = articolo["codice"]
    quantita = articolo.get("quantita", 1)

Quando si lavora con strutture annidate, è importante controllare ogni livello: il payload deve essere un dizionario, articoli deve essere una lista e ogni elemento della lista deve essere un dizionario.

Best practice

  • Non fidarti dei dati esterni: dati provenienti da API, form o file possono essere mancanti o errati.
  • Usa messaggi di errore specifici: indicare il nome del campo facilita il debug e la correzione da parte del client.
  • Non modificare il payload originale: crea un nuovo dizionario normalizzato per separare i dati esterni da quelli interni.
  • Ignora i campi sconosciuti quando opportuno: questa strategia rende il codice più resistente all’evoluzione dell’API.
  • Evita valori mutabili come default: non usare, per esempio, def funzione(dati={}):. Usa invece None e crea il dizionario all’interno.
  • Usa isinstance() per i controlli di tipo: è più chiaro e flessibile rispetto a confronti rigidi con type().
  • Considera Decimal per importi finanziari: float è sufficiente per molti esempi, ma nei pagamenti è preferibile evitare gli errori di precisione binaria.

Riepilogo

I dizionari non servono soltanto a memorizzare coppie chiave-valore: sono uno strumento fondamentale per ricevere, controllare e trasformare dati strutturati. Usando in, get(), isinstance() e dizionari annidati possiamo costruire funzioni affidabili per gestire payload provenienti da sistemi esterni.

La tecnica principale consiste nel separare tre fasi: validazione, raccolta degli errori e normalizzazione. In questo modo il resto dell’applicazione riceve dati coerenti, indipendentemente dalla qualità del payload originale.

Approfondisci con risorse ufficiali

SHARE