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.