Validate the output, always
A model returns text, not data. Parse it at the boundary, check every field you are about to use, and decide what happens when it does not match.
Text that looks like data is still text
You asked for JSON. You got JSON, in your ten manual tests. On the eleventh call you get this:
Sure! Here is the analysis you asked for:
{"sentiment": "positive", "score": 8}
Let me know if you would like me to adjust anything.
json.loads on that raises JSONDecodeError on the very first character. The model did nothing wrong by any definition it was given: it produced a helpful message that contains the object. Your code assumed the string is the object.
This is the same class of problem as parsing a user-uploaded CSV, and it deserves the same treatment. The boundary between "text from outside" and "data my code relies on" needs one function, and everything past that function is trusted because it was checked.
The validation boundary
One function. It takes raw text and returns either the value your code wants, or a clear signal that it did not get one.
import json
def parse_review(raw):
start = raw.find("{")
end = raw.rfind("}")
if start == -1 or end == -1 or end < start:
return None
try:
data = json.loads(raw[start : end + 1])
except ValueError:
return None
if not isinstance(data, dict):
return None
sentiment = data.get("sentiment")
score = data.get("score")
if not isinstance(sentiment, str) or not sentiment:
return None
if isinstance(score, bool) or not isinstance(score, (int, float)):
return None
return {"sentiment": sentiment, "score": score}
Every line is answering a question you would otherwise be answering at 3am.
Locate the object. Take everything from the first { to the last }. Crude, and it handles the overwhelmingly common case of an object wrapped in friendly prose or a code fence.
Parse defensively. json.loads raises on bad input, so the try is how failure becomes a return value instead of an escape.
Check the shape. Valid JSON can be a list, a number, or the string "null". data.get(...) on a list raises AttributeError, so the type of the top-level value is checked before anything is read out of it.
Check every field you will use. Presence, type, and range are three separate questions. A key can be present and None. A score can be a string "8". A rating meant to be 0 to 10 can be 47.
Return one normalized shape. Callers get either the dictionary they expect or None. They never get a half-validated dictionary with two good fields and one surprise.
Deciding what a failure means
Returning None is a decision, not a default. The alternatives are real and the right one depends on what the caller does next.
| Situation | Reasonable response |
|---|---|
| One item in a batch of a thousand | Skip it, count it, log the raw text |
| The user is waiting on this one answer | Retry once, then show an honest failure message |
| The value feeds a payment or a permission | Fail closed, refuse, never guess |
| The field is decorative, like a suggested title | Fall back to a default and carry on |
What is not acceptable is silently substituting a plausible value. A missing score that becomes 0 will be averaged into a dashboard and nobody will ever know the difference between "rated zero" and "never rated". That is the same shape as the swallowed exception from earlier in this level, wearing different clothes.
Sort each field of a parsed model response by whether it needs checking before use.
Ask for a shape that is easy to check
There is a design lever here that is worth more than any parsing cleverness. The narrower the shape you ask for, the cheaper validation is. {"score": 8} is a stronger request than "tell me how positive this is", because you can check the former in three lines and the latter needs a judgment call. A short fixed vocabulary beats free text, and one flat object beats a nested structure whose depth you have to walk.
Interview nuance: if you say "the model returns text, so I would validate it at the boundary and decide what a failure means before I wire it into anything", you have described the same architecture you would use for a webhook, a file upload, or a partner API. Interviewers are checking that you treat the model as a normal untrusted input source, because the candidates who do not are the ones who ship a handler that crashes on the first friendly preamble.
import json
raw = 'Sure! Here is the analysis:\n\n{"sentiment": "positive", "score": 8}\n\nHope that helps.'
try:
print(json.loads(raw))
except ValueError as exc:
print("raw parse failed:", exc)
start = raw.find("{")
end = raw.rfind("}")
print(json.loads(raw[start : end + 1]))Apply
Your turn
The task this lesson builds to.
Write a function parse_review(raw) that turns the text a model returned into a validated
dictionary, or returns None when it cannot.
The contract: raw should contain a JSON object somewhere in it, possibly wrapped in prose. That
object must have a "sentiment" key holding a non-empty string and a "score" key holding a
number. On success return {"sentiment": <the string>, "score": <the number>}. On any failure,
including text that contains no object, text that is not valid JSON, a top-level value that is not
an object, a missing key, or a field of the wrong type, return None.
A boolean counts as a number in Python, so rule it out explicitly. json is already imported in the
starter.
3 hints and 4 automated checks are waiting in the workspace.
Practice
Make it stick
A second problem on the same idea, so it survives past today.
A nightly job asks a model to tag every support ticket, and the tags feed a dashboard the support lead reads each morning. Last week one malformed response reached the dashboard and a whole category of tickets vanished from it for three days. Before the pipeline runs, you now want to know whether every response in the batch is usable, and which one is not.
Write a function first_invalid_index(responses) that returns the index of the first response in
the list that fails validation, or -1 when every response is valid.
A response is valid when it contains a JSON object, possibly wrapped in prose, with an "id" key
holding a positive integer and a "tags" key holding a list in which every element is a string. An
empty tag list is valid. A boolean counts as an integer in Python, so an "id" of true is not
valid. Keep first_invalid_index as the last function in the file.
3 hints and 4 automated checks are waiting in the workspace.