JSON en Pydantic

JSON en Pydantic

27 maart 2024
Erwin Matijsen
Geplaatst in Tutorial
Onderdeel van Werken met JSON


In Werken met JSON leerde je wat JSON is en hoe je er met Python mee kunt werken. Je haalde daar CBS-data op in JSON-formaat, verwerkte dat en toonde het inwoneraantal van Nederland per jaar op je scherm.

Als je met een API werkt, zullen de gegevens heel vaak in JSON worden opgestuurd. Bijna altijd zul je ook de structuur weten van de JSON, of je het nu ontvangt of opstuurt. En ook bijna altijd zul je als je werkt met JSON Python-objecten naar JSON willen omzetten, of juist van JSON naar Python-objecten. Van Python naar JSON noem je serialiseren. Van JSON naar Python noem je deserialiseren.

Als je serialiseert of deserialiseert is het prettig als je weet dat de gegevens voldoen aan jouw verwachting. Stel je verwacht van een API de volgende structuur:

json
[
   {
      "Naam":"JSON",
      "Type":"Gegevensuitwisselingsformaat",
      "isProgrammeertaal":false
   },
   {
      "Naam":"JavaScript",
      "Type":"Programmeertaal",
      "isProgrammeertaal":true
   }
]

Zet je dit om naar Python, dan wil je valideren dat alle gegevens aanwezig zijn, bijvoorbeeld. Of dat extra gegevens die eventueel meekomen niet in je Python-object terechtkomen. Andersom, als je een Python-object hebt dat je naar een API wilt sturen als JSON, dan is het prettig als je er zeker van kunt zijn dat alle verplichte velden aanwezig zijn en van het juiste gegevenstype zijn.

In deze tutorial leer je daarom werken met Pydantic. In hun eigen woorden:

Pydantic is the most widely used data validation library for Python.

Pydantic is een bibliotheek voor het valideren en serialiseren van Python-objecten. Om dit te bereiken maak je gebruik van de ingebouwde type hints van Python. Een voorbeeld zal duidelijk maken hoe het werkt.

python
from pydantic import BaseModel

# Maak een klasse die overerft van Pydantics BaseModel
class User(BaseModel):
    naam: str  # Naam is verplicht en altijd een str
    leeftijd: int  # Leeftijd is verplicht en altijd een int

# User gegevens in een `dict` 
user_dict = {
    "naam": "Guido",
    "leeftijd": 42
}

# Maak een user object aan door de dict uit te pakken
user = User(**user_dict)
print(user)
# Output: User(naam='Guido', leeftijd=42)

# Je kunt nu attributen aanroepen
print(user.naam)  # Guido

# Je kunt een object ook met argumenten aanmaken
user = User(naam="Guido", leeftijd=42)

# Of met model_validate()
user = User.model_validate(user_dict)

# Alle velden zijn verplicht, ontbreekt er één, dan krijg je een ValidationError
user_faux = User(naam="Guido")
# ...
# leeftijd
#   Field required [type=missing, input_value={'naam': 'Guido'}, input_type=dict]


# Of als je een verkeerd type gebruikt
user_faux = User(naam="Guido", leeftijd="vijftig")
# ...
# leeftijd
#   Input should be a valid integer, unable to parse string as an integer [type=int_parsing, input_value='vijftig', input_type=str]

# Zet om in JSON
user_json = user.model_dump_json()
print(user_json)
# Output: {"naam":"Guido","leeftijd":42}

Je ziet, je kunt Pydantic gebruiken om eenvoudig klassen te maken zodat je altijd zeker weet dat je met de juiste gegevens werkt.

Het project

Gebruik hetzelfde project als in Werken met JSON en Cache je HTTP requests. In plaats van de gegevens op te slaan als JSON en te verwerken, ga je ze met Pydantic valideren en verwerken. Het resultaat blijft hetzelfde, namelijk per jaar het totale bevolkingsaantal van Nederland:

output
1950: 10.026.773
1951: 10.200.280
...
2022: 17.590.672
2023: 17.811.291

Voordat je begint

Ga naar het project, activeer de virtual environment en installeer Pydantic:

shell
cd projects/cbs_json
source .venv/bin/activate  # .venv\Scripts\activate.bat voor Windows
python -m pip install pydantic
python -m pip freeze > requirements.txt

Alle code van dit project is ook op Github te bekijken. Omdat deze respository in meerdere tutorials wordt gebruikt, zijn er meerdere branches. Zorg dat je voor deze tutorial versie-3 kiest.

JSON-gegevens ophalen en valideren

In main.py zijn twee functies aanwezig: get_data en get_total_population. De eerste wijziging maak je in get_data. Met deze functie haal je de JSON-gegevens op van het CBS. De wijziging die je maakt is dat je valideert of de inkomende data voldoet aan wat je verwacht, en je gooit direct weg wat je niet nodig hebt.

De JSON die je terugkrijgt, ziet er ongeveer zo uit (sterk ingekort):

CBS JSON
{
  "odata.metadata": "https://opendata.cbs.nl/ODataApi/OData/85496NED/$metadata#Cbs.OData.WebAPI.TypedDataSet", 
  "value": [{"ID": 0, "Perioden": "1950JJ00", "TotaleBevolking_1": 10026773, "Mannen_2": 4998251}]
}

Je bent alleen geïnteresseerd in de waardes in value. Deze waarden staan in een lijst, dus je Response-model kan heel eenvoudig zijn. Maak de volgende klasse aan:

main.py
from pydantic import BaseModel
# ...

class Response(BaseModel):
    value: list

# ...

In get_data schreef je de JSON naar een bestand. Dat vervang je nu door een versie waarin je Pydantic gebruikt.

Oude versie main.py
# ...

def get_data():
    session = requests_cache.CachedSession(expire_after=tonight)
    response = session.get(URL)

    # Als de HTTP-code 400 of hoger is, zal er een uitzondering worden opgeworpen
    response.raise_for_status()
    data = response.json()

    # Sla bestand op
    with open("data.json", "w") as f:
        json.dump(data, f, indent=2)

    return data
Nieuwe versie main.py
from pydantic import BaseModel, ValidationError

# ...

class Response(BaseModel):
    value: list


def get_data():
    session = requests_cache.CachedSession(expire_after=tonight)
    response = session.get(URL)

    # Als de HTTP-code 400 of hoger is, zal er een uitzondering worden opgeworpen
    response.raise_for_status()

    # Valideer de inkomende JSON, behoud alleen de `values`
    try:
        validated_response = Response.model_validate_json(response.content)
    except ValidationError as e:
        print(e)
        return

    return validated_response

Met Response.model_validate_json(response.content) zet je de inkomende JSON om naar het Response-object, en je valideert dit direct. Alle velden die niet in Response zijn gedefineerd, worden genegeerd. Je houdt zo dus enkel de lijst value over.

Print je response.value, dan zie je iets als:

python
print(validated_response.value)
# [{'ID': 0, 'Perioden': '1950JJ00', 'TotaleBevolking_1': 10026773, 'Mannen_2': 4998251, ....

Mocht er nu iets niet goed gaan en de data is niet valide, dan werpt Pydantic een ValidationError op. Is dat het geval, geef dan None terug. Anders geef je de validated_response terug.

Gegevens verwerken

Ook de functie get_total_population kun je wijzigen om met Pydantic te werken. Uit alle data ben je alleen geïnteresseerd in het jaartal en de totale bevolking. Maak een tweede Pydantic-model aan:

main.py
# ...

class Data(BaseModel):
    jaar: str
    totale_bevolking: int


# ...

Het jaartal is een str en de totale bevolking een int.

Wijzig ook de get_total_population-functie.

Oude versie main.py
# ...

def get_total_population():
    # Open het bestand
    with open("data.json") as f:
        data = json.load(f)  # data is nu een Python object (dict)

    # Bereid een lege lijst voor
    result = []

    # Loop door de rijen in de data
    for row in data["value"]:
        year = row["Perioden"].split("JJ00")[0]  # Schoon het jaartal op
        value = row["TotaleBevolking_1"]

        # Voeg het jaartal en de waarde toe aan de lijst als een tuple
        result.append((year, value))

    return result
Nieuwe versie main.py
# ...

def get_total_population(validated_response):
    # Bereid een lege lijst voor
    result = []

    # Loop door de rijen in de data
    for row in validated_response.value:
        jaar = row["Perioden"].split("JJ00")[0]  # Schoon het jaartal op
        totale_bevolking = row["TotaleBevolking_1"]

        try:
            data = Data(jaar=jaar, totale_bevolking=totale_bevolking)
        except ValidationError:
            print("Geen geldige data, sla rij over")
            continue

        # Voeg het jaartal en de waarde toe aan de lijst als een Data instantie
        result.append(data)

    return result

Omdat je nu geen JSON uit een bestand meer inleest, zorg je ervoor dat de functie een parameter validated_response ontvangt. Verder itereer je nu over validated_data.value - Dus het veld value van het Response-model. En tot slot valideer je de data (jaar en bevolking) met het Pydantic-model Data dat je zojuist hebt aangemaakt.

Samenvoegen

Alles is nu klaar. Je kunt nu met get_data de data ophalen en valideren. Is er geldige data, dan ga je verder met get_total_population. Tot slot itereer je over elk item in het resultaat, wat een lijst met Data-objecten is. Dit was eerst een tuple met jaar-bevolking paren. Doordat het nu Data-objecten zijn kun je de velden (attributen) aanroepen, wat het geheel explicieter en dus duidelijker maakt.

Oude versie main.py
# ...

if __name__ == "__main__":
    data = get_data()
    total_population = get_total_population()

    for year, value in total_population:
        print(f"{year}: {value:n}")
Nieuwe versie main.py
# ...

if __name__ == "__main__":
    validated_response = get_data()
    if validated_response:
        total_population = get_total_population(validated_response=validated_response)

        for row in total_population:
            print(f"{row.jaar}: {row.totale_bevolking:n}")

Et voila, hetzelfde resultaat als eerst, maar nu gevalideerd met Pydantic én handiger om mee te werken omdat je met de namen van attributen kunt werken.

Conclusie

In deze tutorial heb je de basis geleerd van het werken met Pydantic. Er is nog veel meer mogelijk, maar in de kern gaat het erom dat je gegevens kunt valideren en er eenvoudiger mee kunt werken. Het kan in vele situaties handig zijn, maar zeker ook als je met API's werkt waar je gegevens mee wilt uitwisselen. Bekijk de documentatie van Pydantic om er meer over te leren.

Alle code van dit project is ook op Github te bekijken. Omdat deze respository in meerdere tutorials wordt gebruikt, zijn er meerdere branches. Zorg dat je voor deze tutorial versie-3 kiest.

Lees alle tutorials in deze reeks: Werken met JSON

Over de auteur


Erwin Matijsen

Erwin Matijsen

Erwin is de oprichter van python-cursus.nl. In allerlei rollen heeft hij Python ingezet, van het eenvoudiger maken van zijn werk tot het opleveren van complete (web)applicaties. Met vrouw en kinderen woont hij in Havelte (Drenthe), midden in de prachtige natuur. Daar wandelt hij graag, zeker ook omdat de beste ingevingen tijdens een wandeling - weg van de computer - lijken te komen.



Contact

Vragen, opmerkingen?

Heb je vragen, opmerkingen, suggesties of tips naar aanleiding van deze blog? Neem dan contact met ons op, of laat het weten via Mastodon of LinkedIN.