Werken met complexe JSON

Werken met complexe JSON

17 februari 2025
Erwin Matijsen
Geplaatst in Tutorial
Onderdeel van Werken met JSON


In voorgaande tutorials heb je geleerd hoe je met JSON werkt in Python, hoe je HTTP-requests kunt cachen en hoe je JSON kunt valideren met Pydantic. Je hebt toen gewerkt met relatief eenvoudige JSON-data van het CBS. Maar in de praktijk kom je vaak complexere JSON-structuren tegen, bijvoorbeeld bij het analyseren van verkoopdata of het verwerken van rapportages.

Stel je voor: je krijgt een JSON-bestand met verkoopcijfers van je bedrijf, bijvoorbeeld via de API van jullie webshop. Het bevat informatie over verschillende productcategorieën, met per product de verkoopaantallen, inkoopprijs, verkoopprijs en klanttevredenheidsgegevens. De JSON die je moet verwerken ziet er ongeveer zo uit:

{
  "2024": {
    "q1": {
      "producten": [
        {
          "categorie": "Elektronica",
          "items": [
            {
              "naam": "Laptop",
              "verkopen": {
                "jan": 150,
                "feb": 180,
                "mrt": 210
              },
              "inkoopprijs": {
                "jan": 800,
                "feb": 800,
                "mrt": 750
              },
              "verkoopprijs": {
                "jan": 999,
                "feb": 999,
                "mrt": 899
              },
              "klanttevredenheid": {
                "score": 4.2,
                "reviews": [
                  {
                    "datum": "2024-01-15",
                    "score": 4,
                    "tekst": "Goede laptop voor deze prijs"
                  },
                  {
                    "datum": "2024-02-03",
                    "score": 5,
                    "tekst": "Perfecte prijs-kwaliteitverhouding"
                  },
                  {
                    "datum": "2024-03-12",
                    "score": 3,
                    "tekst": "Batterij gaat niet zo lang mee als verwacht"
                  }
                ]
              }
            }
          ]
        }
      ]
    }
  }
}

Zoals je ziet, is dit complexer dan de CBS-data. Je hebt te maken met meerdere niveaus van geneste objecten (objecten in objecten), lijsten van objecten en verschillende datatypen. Hoe bereken je bijvoorbeeld de totale omzet per categorie? Of hoe vind je de producten met de hoogste klanttevredenheid?

Voordat je begint

Om te oefenen met de complexere data ga je een aantal analyses maken, zoals het berekenen van de omzet per categorie. Maak hiervoor eerst een nieuw project aan. Je hebt geen extra packages nodig:

cd projects
mkdir verkoopanalyse && cd verkoopanalyse
python -m venv .venv
source .venv/bin/activate  # .venv\Scripts\activate.bat voor Windows
python -m pip freeze > requirements.txt

Download het bestand verkoopdata.json uit de Github-repository, en sla dit op in je projectmap. Maak ook alvast een main.py aan.

Github

Alle code in deze tutorial - en de bijbehorende data - is te vinden in de Github repository.

De JSON-structuur begrijpen

De allereerste stap in het werken met JSON is altijd dat je ervoor zorgt dat je de algemene structuur begrijpt, zodat je weet waar je mee werkt. Open het JSON-bestand in een editor en inspecteer het.

Je ziet dat de JSON-data een hiërarchische structuur heeft:

  • Op het hoogste niveau staat het jaar (2024)
  • Daaronder het kwartaal (q1)
  • Dan een lijst met producten
  • Elke product heeft een categorie en een lijst met items
  • Elk item bevat:
    • naam: de productnaam
    • verkopen: een dict met verkoopaantallen per maand
    • inkoopprijs: een dict met inkoopprijzen per maand
    • verkoopprijs: een dict met verkoopprijzen per maand
    • klanttevredenheid: met daarin een gemiddelde score en lijst met reviews

Door JSON-structuren navigeren

Nu je weet hoe de JSON in elkaar steekt, kun je ermee aan de slag. De volgende code in main.py bevat enkele eenvoudige handelingen om te laten zien wat je met de data kunt.

main.py
import json


def laad_verkoopdata():
    with open("verkoopdata.json") as f:
        return json.load(f)


if __name__ == "__main__":
    # Laad de data
    verkoopdata = laad_verkoopdata()

    # Bekijk eerst de structuur
    print("Structuur van de verkoopdata:")
    print(json.dumps(verkoopdata, indent=2))  # indent geeft aan met hoeveel spaties je regels wilt inspringen

    # Voorbeeld: navigeer naar de eerste categorie
    eerste_product = verkoopdata["2024"]["q1"]["producten"][0]
    print(f"\nEerste categorie: {eerste_product['categorie']}")

    # Voorbeeld: haal alle categorieën op
    categorieen = []
    for product_categorie in verkoopdata["2024"]["q1"]["producten"]:
        categorie = product_categorie["categorie"]
        categorieen.append(categorie)
    print(f"\nAlle categorieën: {categorieen}")

Het begint met een eenvoudige functie om de verkoopdata in te laden. Vervolgens gebruik je json.dumps om de JSON-structuur te bekijken. Uiteraard kun je ook gewoon het JSON-bestand openen in je editor - zoals je hiervoor deed - maar soms is het handig om iets te kunnen tonen in je script.

Hierna volgen twee voorbeelden hoe je met de geneste structuur werkt. Omdat verkoopdata een dict is (na het inladen), kun je [<sleutel>] gebruiken om elementen op te halen. Dus, in het eerste voorbeeld haal je de eerste categorie op door de volgende stappen te nemen:

  • Eerst haal je het jaar op met verkoopdata["2024"]
  • Dit levert een nieuwe dict op, waar je de sleutel "q1" (het eerste kwartaal) ophaalt met ["q1"]
  • Dit levert weer een nieuwe dict op met als sleutel "producten". Die haal je op met ["producten"]
  • Dit levert tot slot een lijst met producten op, waarvan je de eerste ophaalt met [0]

Samen wordt dit dus: eerste_product = verkoopdata["2024"]["q1"]["producten"][0]. Hiermee heb je het eerste product opgehaald, welke je opslaat in de variabele eerste_product. Om hiervan tot slot de categorie op te halen voer je eerste_product["categorie"] uit.

Je ziet dat je om een categorie op te halen, je enkele lagen diep moet en dat je zowel gegevens uit dicts en een list moet halen.

Dit geldt ook voor het tweede voorbeeld, waarin je álle categorieën ophaalt. Je begint met het maken van een lege lijst: categorieen = []. Vervolgens loop je over alle producten, sla je de categorie van het product op met categorie = product["categorie] en voeg je dit toe aan de lijst met categorieen.append(categorie).

Het werken met complexere JSON-structuren is dus feitelijk het werken met complexere dicts. Het is vooral de kunst de structuur goed te begrijpen en weten wat je wilt ophalen uit een diep-gelaagde dict.

Veilig navigeren

In de twee voorgaande voorbeelden moest je meerdere lagen diep om de gewenste gegevens op te halen. Je gebruikt daarvoor [<sleutel>][<sleutel>][<sleutel>], en dat werkt goed omdat de data waarmee je werkt bekend is. Maar wat als de data waar je mee werkt minder betrouwbaar is? Het komt bijvoorbeeld binnen via een API, en niet alle gegevens zijn altijd aanwezig. Gebruik je dan [<sleutel>] voor een sleutel die niet bestaat, dan ontstaat er een fout.

Er zijn verschillende manieren om hiermee om te gaan. Hieronder zie je er twee.

main.py
def vind_product_met_get(data, productnaam):
    """
    Zoek een product op naam met de .get() methode.
    Return None als het product niet gevonden wordt.
    """
    producten = data.get("2024", {}).get("q1", {}).get("producten", [])

    for categorie in producten:
        items = categorie.get("items", [])
        for product in items:
            if product.get("naam") == productnaam:
                return product

    return None

def vind_product_met_try_except(data, productnaam):
    """
    Zoek een product op naam met try-except blokken.
    Return None als het product niet gevonden wordt.
    """
    try:
        for categorie in data["2024"]["q1"]["producten"]:
            for product_categorie in categorie["items"]:
                if product_categorie["naam"] == productnaam:
                    return product_categorie
    except KeyError:
        return None

    return None

if __name__ == "__main__":
    verkoopdata = laad_verkoopdata()

    # ...

    # Toon de klanttevredenheid van een laptop, als het product bestaat
    laptop_data = vind_product_met_get(verkoopdata, "Laptop")
    # laptop_data = vind_product_met_try_except(verkoopdata, "Laptop")
    if laptop_data:
        print(f"Gevonden! Klanttevredenheid: {laptop_data['klanttevredenheid']['score']}")
    else:
        print("Product niet gevonden")

In dit voorbeeld zie je twee methoden om veilig door JSON te navigeren.

De eerste methode gebruikt .get() om waardes op te halen. .get() geeft de waarde terug als de sleutel is gevonden, of anders een standaard waarde als de sleutel niet bestaat. Standaard is die standaardwaarde None, maar je kunt het instellen op een lege dict ({}). Bijvoorbeeld: data.get("2024", {}).

Bestaat de sleutel "2024" wel, dan is het resultaat de waarde behorende bij die sleutel (een dict met als sleutel "q1" in dit geval). Bestaat de sleutel "2024" niet, dan wordt een lege dict teruggegeven. In beide gevallen kun je .get("q1", {}) aanroepen, zonder dat het een fout oplevert. Dit trucje herhaal je totdat je de sleutel hebt bereikt die je wilt hebben; "producten" in dit geval.

Let op dat dit alleen werkt omdat je weet dat de eerste lagen allemaal dicts zijn. Dus je weet dat als je .get("2024") aanroept, dat je een dict als waarde terugkrijgt, waar je ook weer .get() op kunt aanroepen. Zou er een ander object worden teruggegeven - zoals een list- dan ontstaat er alsnog een fout.

De tweede methode gebruikt een try-except-clausule. Als één van de sleutels niet gevonden wordt zal er een KeyError ontstaan en geeft de functie None terug. Stel dat je 'Laptop' zoekt, maar de eerste sleutel 2024 bestaat niet (omdat je alleen data hebt over 2023), dan volgt dus ook een KeyError. Het lijkt dan alsof je de laptop niet gevonden hebt, maar feitelijk heb je het jaar niet gevonden. Je zou de functie kunnen aanpassen naar het volgende om duidelijker te maken wélke sleutel niet gevonden is:

    try:
        # rest van code
    except KeyError as e:
        print(f"Sleutel {e} niet gevonden")

Vaak zul je de algemene structuur van de JSON waar je mee gaat werken wel kennen, en kun je goed anticiperen op eventuele missende sleutels of waarden. Daar zijn verschillende tactieken voor, zoals het controleren op sleutels met try-except of werken met .get(). Het is afhankelijk van de situatie hoe veel je moet controleren en welke tactiek het beste werkt.

In deze voorbeelden voer je de controles uit in de analysefuncties. Je kunt de stappen ook scheiden, door in een eerste stap - na het inladen van je gegevens - allerlei controles uit te voeren. Je kunt bijvoorbeeld controleren of alle data die je verwacht aanwezig is en in het goede formaat is. Mist er data, dan kun je hier iets mee doen, bijvoorbeeld standaard lege waardes in te vullen (zoals een lege dict, None of een lege list). In je analyses hoef je de controles dan niet meer uit te voeren.

Verkoopdata analyseren

Nu je weet hoe je door de JSON-structuur kunt navigeren, kun je beginnen met het analyseren van de data. Door een paar analyses te maken, zul je moeten nadenken over de structuur van de JSON en hoe je de gewenst gegevens eruit krijgt. Probeer de volgende analyses eerst zelf eens te maken, voordat je naar de uitwerking kijkt. En bedenk, er zullen vaak meerdere manieren mogelijk zijn om de analyses uit te voeren. De hier getoonde uitwerkingen zijn dus niet de enige manier!

Omzet berekenen

Een eerste interessante analyse is het berekenen van de totale omzet per categorie. De omzet bereken je door de verkopen per maand te vermenigvuldigen met de verkoopprijs per maand.

main.py
def omzet_per_categorie(data):
    """ Verkrijg totale omzet per categorie"""
    producten = data["2024"]["q1"]["producten"]

    # Maak een lege `dict` om het resultaat in op te slaan
    resultaat = {}

    # Loop over alle productgroepen
    for product_categorie in producten:
        categorie = product_categorie["categorie"]
        totale_omzet = 0

        # Loop over alle items in de productgroepen
        for item in product_categorie["items"]:

            # Loop door de maanden
            for maand in ["jan", "feb", "mrt"]:
                verkopen = item["verkopen"][maand]
                verkoopprijs = item["verkoopprijs"][maand]
                omzet = verkopen * verkoopprijs
                totale_omzet += omzet

        # Sla het resultaat per categorie op
        resultaat[categorie] = totale_omzet

    return resultaat


if __name__ == "__main__":
    resultaat = omzet_per_categorie(verkoopdata)
    for categorie, omzet in resultaat.items():
        bedrag = locale.format_string("€%.2f", omzet, grouping=True, monetary=True)
        bedrag = bedrag.replace(" ", ".")
        print(f"{categorie}: {bedrag}")

In de functie omzet_per_categorie zie je dat je een aantal lagen diep moet om de gewenste gegevens ("verkoopprijs" en "verkopen") te verkrijgen. Dit is slechts één voorbeeld van wat je kunt met deze data. Het is vooral bedoeld ter illustratie hoe je met geneste data kunt werken. Als je het leuk vindt kun je nog andere analyses maken, zoals:

  • De gemiddelde klanttevredenheid per categorie
  • Het aantal reviews per product
  • De marge (verkoopprijs - inkoopprijs) per product of categorie
  • Best verkochte product (qua omzet of aantal verkochte items)

Conclusie

In deze tutorial heb je geleerd hoe je kunt werken met complexere JSON-structuren in Python.

Allereerst is het essentieel om de structuur van je JSON-data goed te begrijpen voordat je ermee aan de slag gaat. Neem de tijd om de hiërarchie van de data te bekijken - welke objecten zijn genest, waar zitten de lijsten, en hoe diep moet je gaan om bepaalde informatie te bereiken?

Het navigeren door JSON-data is feitenlijk het werken met Python dicts. Door gebruik te maken van vierkante haken ([]) kun je steeds een niveau dieper in de structuur gaan. Maar let op: als je werkt met data die niet 100% betrouwbaar is, bijvoorbeeld omdat deze van een externe API komt, dan is het verstandig om beschermingsmechanismen in te bouwen.

Voor die betrouwbaarheid heb je twee benaderingen bekeken in deze tutorial:

  1. De .get()-methode, die je een standaardwaarde laat specificeren als een sleutel niet bestaat
  2. try-except blokken, die je helpen om netjes om te gaan met ontbrekende data

Tot slot heb je gezien hoe je deze kennis kunt toepassen in een praktisch voorbeeld: het analyseren van verkoopdata. Door systematisch door de data te lopen en de juiste waarden te combineren, kun je analyses maken zoals het berekenen van de omzet per categorie.

Github

Alle code in deze tutorial - en de bijbehorende data - is te vinden in de Github repository.

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.