Skip to main content

Validate the output, always

Level 5: Level 5: Judging Code You Did Not Writemedium24 mininput validationjson parsingdefensive programmingapi integration

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.

Check yourself
Your code does data = json.loads(response) and the response begins with 'Sure! Here is the analysis:'. What happens?

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.

Check yourself
A validator checks the score field with isinstance(score, (int, float)). The model returns {'sentiment': 'positive', 'score': true}. What happens?

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.

SituationReasonable response
One item in a batch of a thousandSkip it, count it, log the raw text
The user is waiting on this one answerRetry once, then show an honest failure message
The value feeds a payment or a permissionFail closed, refuse, never guess
The field is decorative, like a suggested titleFall 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.

Check yourself

Sort each field of a parsed model response by whether it needs checking before use.

A score you are about to average into a dashboard
A category name you look up in a dictionary of handlers
The result of json.dumps on a value you built yourself
A row identifier you are about to write into a database query
A constant defined at the top of your own module

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.

Check yourself
Your validator finds a response where score is present but is the string '8'. What is the right move?
Worked example (Python)
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.