Read Nested JSON with Explicit Missing-Field Rules
Extract a city from nested JSON data while distinguishing a missing optional value from an invalid object shape.
Example check: Worked and guided code samples are checked against CPython 3.13. Practice uses Pyodide 0.29.5 (Python 3.13) in your browser. How we check lessons
Video lesson: Read Nested JSON with Explicit Missing-Field Rules
The preview is stored on this site. YouTube loads only when you press Play. Watch on YouTube
Jump to a chapter
Read the video transcript
Select a timestamp to watch that moment on YouTube.
- 00:00
A JSON document can contain an address with a city, omit the address, or place a completely different kind of value at that path. A quick nested-index expression works on one perfect sample and often fails on the first real variation. Today's function has a precise rule: missing optional address or city means no location and returns None; a present value with the wrong shape is an error. Before writing code, say those cases out loud so the fix does not collapse them into one silent fallback. JSON can encode nested objects, arrays, and null values, but json.loads only parses syntax.
- 00:41
It does not promise that a field exists or has the expected Python type. An API may return a user object with an address and city, no address at all, or an address field that is a list by mistake. A safe extractor distinguishes the allowed missing case from an invalid shape. The contract in this lesson returns None when address or city is absent, but raises ValueError when a present container or city value has the wrong type. The broken run raises KeyError for address. If the key existed with a JSON null, Python would see None and the next subscript would fail in another way. A blanket except around every possible error would hide which assumption was
- 01:23
violated. Instead, inspect the path one level at a time. The first question is whether the root is a dictionary. The second is whether user is a dictionary. Only then should we ask whether address is absent or present. The broken expression record['user']['address']['city'] works only on the happy path. Missing keys raise KeyError; a null address produces TypeError; a list instead of a dictionary can fail differently. Chaining get calls blindly does not solve all of this because get on a non- dictionary still fails and can hide unexpected input. Print the types at each boundary and define what the product considers
- 02:04
optional. Here the root and user must be objects, while address and city may be absent. The diagnosis prints NoneType for one sample and list for another. Both are valid Python values, and both can come from syntactically valid JSON, but they mean different things under our contract. None is allowed here as optional absence. A list is not a valid address object. Calling get blindly on a list would raise an accidental attribute error, while returning None for that list would conceal broken upstream data. This is why parsing syntax and validating shape should be separate steps.
- 02:45
Start with a helper that checks isinstance(record, dict) and isinstance(user, dict). Read address with get. If it is None because the field is missing or explicitly null, return None according to the stated policy. If it is present but not a dictionary, raise ValueError. Read city similarly; if present it must be a nonempty string. This nested validation is ordinary Python and works after json.loads or with an already parsed dictionary. Do not silently convert numbers to city names or treat malformed objects as missing. The corrected function checks each container before going deeper.
- 03:26
It uses get only after verifying it has a dictionary. It returns None for optional missing values, but raises ValueError for a present address that is not an object or a present city that is not nonempty text. We do not call str on a numeric city because that would invent a location name from invalid data. This can be used after json.loads or with an already parsed object in the browser exercise. Validation order matters. If a root is a list, trying record.get would itself fail, so inspect root before using dictionary methods. If user is absent, this example treats that as a malformed required field rather
- 04:07
than an optional location. Address and city are optional; a present wrong type is malformed. Error messages should identify the field that failed, but avoid placing sensitive user data inside them. The browser checks verify both the allowed missing path and the rejected malformed path, so a solution that merely chains get calls cannot pass. A common shortcut uses chained get calls with an empty dictionary as every fallback. That can return None for a malformed list, accidentally presenting bad data as an ordinary missing value. The explicit branches are longer, yet each branch corresponds to a sentence in the
- 04:47
contract. If a future API schema changes, you can update one branch and its test. In production, you might use a schema validation library, but this small exercise builds the reasoning that such libraries encode. Keep the difference between absence and invalid type in the final explanation and ask which one should alert the caller. The final run proves that a missing address returns None and a list address is rejected with a clear message. Add a null city, an integer city, an empty string, and a valid name. Each test should line up with the contract written above the function.
- 05:27
Complete the site's checked task and quiz, then compare this explicit approach with the direct indexing expression from the opening. The difference is not only fewer exceptions; it is a more informative model of the data producer's promises and failures. The checked task takes a parsed Python object so the browser console can focus on traversal. A solved example also calls json.loads to show the parsing boundary. Test valid city text, missing address, missing city, null address, a list in place of address, and an integer city. The return value None has a documented meaning: optional location unavailable.
- 06:08
The exception means the producer violated the expected shape. Keeping those states distinct makes later error reporting and debugging much clearer.
Understand the concept
JSON can encode nested objects, arrays, and null values, but json.loads only parses syntax. It does not promise that a field exists or has the expected Python type. An API may return a user object with an address and city, no address at all, or an address field that is a list by mistake. A safe extractor distinguishes the allowed missing case from an invalid shape. The contract in this lesson returns None when address or city is absent, but raises ValueError when a present container or city value has the wrong type.
The broken expression record['user']['address']['city'] works only on the happy path. Missing keys raise KeyError; a null address produces TypeError; a list instead of a dictionary can fail differently. Chaining get calls blindly does not solve all of this because get on a non-dictionary still fails and can hide unexpected input. Print the types at each boundary and define what the product considers optional. Here the root and user must be objects, while address and city may be absent.
Start with a helper that checks isinstance(record, dict) and isinstance(user, dict). Read address with get. If it is None because the field is missing or explicitly null, return None according to the stated policy. If it is present but not a dictionary, raise ValueError. Read city similarly; if present it must be a nonempty string. This nested validation is ordinary Python and works after json.loads or with an already parsed dictionary. Do not silently convert numbers to city names or treat malformed objects as missing.
The checked task takes a parsed Python object so the browser console can focus on traversal. A solved example also calls json.loads to show the parsing boundary. Test valid city text, missing address, missing city, null address, a list in place of address, and an integer city. The return value None has a documented meaning: optional location unavailable. The exception means the producer violated the expected shape. Keeping those states distinct makes later error reporting and debugging much clearer.
- JSON parsing is not schema validation
- Optional and malformed values are different states
- Check each nested container before using it
- Return None only for an explicitly allowed absence
See it step by step
Read the code, predict the output, then compare it with the result.
01. Parse a nested JSON object
import json
record = json.loads('{"user": {"address": {"city": "Bratislava"}}}')
print(record['user']['address']['city'])
BratislavaParsing creates dictionaries, but the indexing succeeds only because this specific sample has the expected shape.
02. See a missing optional field
record = {'user': {}}
print(record['user'].get('address'))
NoneAn absent address yields None under the lesson's optional-field policy.
03. Recognize malformed present data
record = {'user': {'address': ['Oslo']}}
print(isinstance(record['user']['address'], dict))
FalseA list is present but is not the required object shape.
A closer look
Follow the reasoning, inspect each result, then try the suggested changes in the console below.
Separate parsing from schema
JSON can encode nested objects, arrays, and null values, but json.loads only parses syntax. It does not promise that a field exists or has the expected Python type. An API may return a user object with an address and city, no address at all, or an address field that is a list by mistake. A safe extractor distinguishes the allowed missing case from an invalid shape. The contract in this lesson returns None when address or city is absent, but raises ValueError when a present container or city value has the wrong type.
Change city to a number and note that json.loads still succeeds. Syntax validity is only the first step. A producer and consumer need a shared data contract for fields and types.
import json
record = json.loads('{"user": {"address": {"city": "Bratislava"}}}')
print(record['user']['address']['city'])
Bratislava- Parse a valid JSON sample.
- Identify the assumed nested fields.
Define optional absence
The broken expression record['user']['address']['city'] works only on the happy path. Missing keys raise KeyError; a null address produces TypeError; a list instead of a dictionary can fail differently. Chaining get calls blindly does not solve all of this because get on a non-dictionary still fails and can hide unexpected input. Print the types at each boundary and define what the product considers optional. Here the root and user must be objects, while address and city may be absent.
A missing address and a JSON null value become None in Python. This lesson accepts both as optional absence. A different product may distinguish them, but that choice must be stated explicitly.
record = {'user': {}}
print(record['user'].get('address'))
None- Use get for an optional field.
- Decide what None means.
Reject present invalid shapes
Start with a helper that checks isinstance(record, dict) and isinstance(user, dict). Read address with get. If it is None because the field is missing or explicitly null, return None according to the stated policy. If it is present but not a dictionary, raise ValueError. Read city similarly; if present it must be a nonempty string. This nested validation is ordinary Python and works after json.loads or with an already parsed dictionary. Do not silently convert numbers to city names or treat malformed objects as missing.
Check the type before using dictionary methods. A list can be valid JSON yet invalid at this particular path. Return a clear ValueError instead of allowing an accidental attribute or key exception.
record = {'user': {'address': ['Oslo']}}
print(isinstance(record['user']['address'], dict))
False- Check a present address is a dictionary.
- Reject a malformed city value.
Try it in Python
Edit the example and run it. Python starts in your browser the first time you click Run.
Python console
Ready to runNeed input()? Add one value per line
Your output appears here.
Need a hint?
Validate root and user with isinstance(..., dict). Read address = user.get('address'); return None if address is None, otherwise require dict. Apply the same optional rule to city, then require a nonempty str.
Complete the task and select Check task to verify your code.
Quick quiz
Three questions. You can change your answers and try again.
Typical mistakes
Everyone meets these errors. See what causes them and how to fix them.
Indexing every field directly
record['user']['address']['city']address = user.get('address')
if address is None:
return NoneWhat happens: Missing keys or null values raise unrelated KeyError or TypeError exceptions.
Handle optional absence before deeper traversal.
Chaining get without checking types
record.get('user', {}).get('address', {}).get('city')if not isinstance(address, dict):
raise ValueError('address must be an object')What happens: A present list or null can still raise AttributeError or mask a malformed shape.
Verify each present container's type explicitly.