TypeError: Object of type dict_values is not JSON serializable

Viewed 6581

I've been working with Python and Requests module to get all of Irelands Covid data by location. I've ran into a problem were my dict which is getting the data from the API call is not able to be converted to JSON, which I want to show on a Pandas Dataframe

Now, unfortunately the website for the API is limited and I have to use a for loop for each location to get the data for that area. Then when I do this, I push this to a dictionary thisdict["of that locations name"]

Then I try convert the all 25 Dicts that it creates to JSON

toJSON = json.dumps(thisdict.values())
data = json.loads(toJSON)

Now this is where I get the error However, if I use the location of one of the places that I have created it will work but I want all the locations. Is this possible

toJSON = json.dumps(thisdict["Dublin"])
data = json.loads(toJSON)

I've Tried

toJSON = json.dumps(*thisdict)
data = json.loads(toJSON)

AND

toJSON = json.dumps(list(thisdict.values())
data = json.loads(toJSON)

Which I found here https://markhneedham.com/blog/2017/03/19/python-3-typeerror-object-type-dict_values-not-json-serializable/

All Code is in this link https://replit.com/@MrGallen/GetC19ApiToCSVEveryCounty#main.py

 # to handle  data retrieval
import requests
# to manage json data
import json
# for pandas dataframes
import pandas as pd

counties = ["Carlow", "Cavan", "Clare", "Cork", "Donegal", "Dublin", "Galway", "Kerry", "Kildare", "Kilkenny", "Laois", "Leitrim", "Limerick", "Longford", "Louth", "Mayo", "Meath", "Monaghan", "Offaly", "Roscommon", "Sligo", "Tipperary", "Waterford", "Westmeath", "Wexford", "Wicklow"]
thisdict = {}
for county in counties:
  url = "https://services1.arcgis.com/eNO7HHeQ3rUcBllm/arcgis/rest/services/Covid19CountyStatisticsHPSCIreland/FeatureServer/0/query?where=CountyName%20%3D%20'"+county+"'&outFields=CountyName,PopulationCensus16,TimeStamp,ConfirmedCovidCases,PopulationProportionCovidCases,ConfirmedCovidDeaths,ConfirmedCovidRecovered&returnGeometry=false&outSR=4326&f=json"
  r = requests.get(url, stream=True)
  #info = r.headers
  #print(info)
  r = r.json()
  r = r["features"]
  thisdict[county] = r

# decode json data into a dict object
toJSON = json.dumps(thisdict)
data = json.loads(toJSON)
# in this dataset, the data to extract is under 'features'
with open("sample.json", "w") as outfile: 
    json.dump(data, outfile)

df = pd.json_normalize(data)
print(df.head(10))

# Select a number of columns - all rows
CD = df[['attributes.CountyName', 'attributes.TimeStamp', 'attributes.ConfirmedCovidCases']]

print(CD) # DataFrame
1 Answers

The real issue here is the general format you're putting that dict in to begin with. You end up nesting multiple lists unnecessarily, when all you need is one big list of county attributes.

The response.json()["features"] is a list of attributes. Carlow, for example, returns a list of 407 attributes for that county. So in your thisdict you end up with a dict of county keys the values of which are all list of attributes.

Then, you try to take the dict_values of that dict, which (if you cast dict_values as a list) yields an unnecessary list of lists of attributes.

This actually works, in terms of (de)serializing into/out of JSON, e.g. this does not throw a serialization exception:

toJSON = json.dumps(list(thisdict.values()))
data = json.loads(toJSON)

However, you will run into problems later when you try to pass this list of lists of dicts to pandas.json_normalize():

Traceback (most recent call last):
  File "/home/dephekt/pandas/main.py", line 25, in <module>
    df = pd.json_normalize(data)
  File "/home/dephekt/pandas/.venv/lib/python3.8/site-packages/pandas/io/json/_normalize.py", line 270, in _json_normalize
    if any([isinstance(x, dict) for x in y.values()] for y in data):
  File "/home/dephekt/pandas/.venv/lib/python3.8/site-packages/pandas/io/json/_normalize.py", line 270, in <genexpr>
    if any([isinstance(x, dict) for x in y.values()] for y in data):
AttributeError: 'list' object has no attribute 'values'

You can see in the traceback it's expecting that data is a list of dicts or a dict. It takes for y in data and then tries to call y.values(), assuming y was a dictionary, but since in this case it's a list nested in a list and a list has no values() method, it throws an AttributeError.

Consider the following code instead:

import json
import logging

import pandas as pd

import requests

logging.basicConfig(level=logging.DEBUG)

counties = [
    "Carlow",
    "Cavan",
    "Clare",
    "Cork",
    "Donegal",
    "Dublin",
    "Galway",
    "Kerry",
    "Kildare",
    "Kilkenny",
    "Laois",
    "Leitrim",
    "Limerick",
    "Longford",
    "Louth",
    "Mayo",
    "Meath",
    "Monaghan",
    "Offaly",
    "Roscommon",
    "Sligo",
    "Tipperary",
    "Waterford",
    "Westmeath",
    "Wexford",
    "Wicklow",
]

results = []
s = requests.Session()

for county in counties:
    url = f"https://services1.arcgis.com/eNO7HHeQ3rUcBllm/arcgis/rest/services/Covid19CountyStatisticsHPSCIreland/FeatureServer/0/query"
    fields = "CountyName,PopulationCensus16,TimeStamp,ConfirmedCovidCases,PopulationProportionCovidCases,ConfirmedCovidDeaths,ConfirmedCovidRecovered"
    params = {
        "where": f"CountyName='{county}'",
        "outFields": fields,
        "returnGeometry": False,
        "outSR": 4326,
        "f": "json",
    }
    response = s.get(url, params=params, timeout=10)
    response.raise_for_status()

    features = response.json().get("features")

    for feature in features:
        results.append(feature)

with open("sample.json", "w") as outfile:
    outfile.write(json.dumps(results))

df = pd.json_normalize(results)
print(df.head(10))

CD = df[["attributes.CountyName", "attributes.TimeStamp", "attributes.ConfirmedCovidCases"]]

print(CD)

What I do here is I make a requests session (see below for more on that) and then for each county's response, we get the list of attributes from the features key, then iterate on that list and put the individual attributes into our results list.

What you end up with is one big list of all the attribute dicts for all the counties.

Then, we avoid some unnecessary gymnastics where you were doing json.dumps() just to then json.loads() it back into a format you already had, e.g.:

toJSON = json.dumps(thisdict)
data = json.loads(toJSON)

Here, data ends up being identical to thisdict, there's no reason to deserialize it back into a dict. You only needed to dump the results to write them to the file. For your pandas arguments you can just pass thisdict (or in my example, results which is a list).

This all ends up with this as output:

  attributes.CountyName  ...  attributes.ConfirmedCovidRecovered
0                Carlow  ...                                None
1                Carlow  ...                                None
2                Carlow  ...                                None
3                Carlow  ...                                None
4                Carlow  ...                                None
5                Carlow  ...                                None
6                Carlow  ...                                None
7                Carlow  ...                                None
8                Carlow  ...                                None
9                Carlow  ...                                None

[10 rows x 7 columns]

      attributes.CountyName  ...  attributes.ConfirmedCovidCases
0                    Carlow  ...                               0
1                    Carlow  ...                               0
2                    Carlow  ...                               0
3                    Carlow  ...                               0
4                    Carlow  ...                               0
...                     ...  ...                             ...
10577               Wicklow  ...                            4460
10578               Wicklow  ...                            4473
10579               Wicklow  ...                            4483
10580               Wicklow  ...                            4491
10581               Wicklow  ...                            4497

[10582 rows x 3 columns]

You can play around more with the format before you ship it to your pandas dataframe if it's not exactly what you want, but hopefully this gives you a good example to work from.

Since you're making a bunch of requests in a loop, I've created a requests.Session() object because that leverages urllib3's connection pooling and reuses the same underlying TCP connection for each GET request, which has better performance. This way you're not renegotiating a new TCP connection for every loop execution. It makes one connection and sends all 26 requests over that, not 26 TCP connections each sending one tiny request.

I also split up your request query parameters from the actual URL endpoint, just for my own sanity. You can do the URL the way you were doing it, but this does give you a little more flexibility in the event you need to alter the value of those parameters. It will work just as well with your long URL, I just found it unwieldy.

Related