Skip to content

← Config, Logs and Untrusted Input step 2 of 16

Easy Primitives

Turn Outside Data into a Typed Record

Your web app receives a sign-up request as JSON. After json.loads, you have a dict of whatever the client sent. The quick way to turn it into a record runs without an error, and mypy accepts it, because json.loads returns Any:

import json
from dataclasses import dataclass


@dataclass(frozen=True)
class Signup:
    username: str
    age: int
    newsletter: bool


body = json.loads('{"username": "ada", "age": "36", "newsletter": "false"}')
signup = Signup(**body)
print(signup)  # Signup(username='ada', age='36', newsletter='false')
print(signup.age + 1)  # TypeError: can only concatenate str (not "int") to str

The type hints say age is an int, but Python does not check them when it builds the object. The bad value gets in, and the crash happens later, far from the request that caused it. Check the data once, where it enters, and the rest of your code can trust every Signup it gets.

What to write

Fill in parse_signup(data). It returns a Signup, or raises the supplied BadField(field, kind) for the first problem it finds.

  • Check the fields in this order: username, age, newsletter. Stop at the first bad one.
  • If a field is not in data, raise BadField(field, "missing").
  • If a field is there but its value has the wrong type, raise BadField(field, "type"). username must be a str, age an int, and newsletter a bool.
  • Ignore any other keys in data.
  • Do not convert values. The string "36" is a type error for age, and the string "false" is a type error for newsletter.

Edge cases

  • JSON true becomes Python True, and bool is a subclass of int, so isinstance(True, int) is True. An age of True is a type error.
  • JSON null becomes None. A field whose value is None is there, so it is a type error, not a missing field.
  • A float such as 36.0 is a type error for age.
  • Any string is a valid username, and any int is a valid age. This exercise has no range checks.

What the driver reports

solve(data) calls your function. On success it returns "ok": True and the three fields. When parse_signup raises BadField, solve returns "ok": False with the error's field and kind.