JSON APIs and validation · Backend Development
What should an API do when the input is wrong?
Course contentsWhy you need it
Anything can arrive at your server: typos, missing fields, text where a number should be, or someone deliberately trying to break it. An API that checks its input and explains every problem clearly is pleasant to build on; one that crashes or quietly stores junk causes bugs far away from the real cause.
The intuition
A request that creates something goes through the same steps every time:
- Parse: turn the JSON text into Python data with
json.loads. If it isn't valid JSON →400 {"error": "invalid JSON"}. - Validate: check every field: present? right type? in range? Collect all the problems in a list, so the client can fix them in one go →
400 {"errors": [...]}. - Act: store it.
- Respond:
201with the new object, including theidthe server gave it.
Validation is a plain function: data in, list of errors out. An empty list means valid.
Never trust the client: it might not be your app. Check on the server even if the web page checks too.
Definition
JSON body
The data a client sends with POST or PUT, as JSON text. json.loads turns it into dicts and lists.
Validation
Checking every field of the input before using it, and reporting what's wrong.
Examples
Text to data and back
Try it yourselfjson.loads parses, json.dumps writes, and bad JSON raises an error you can catch.
import json
data = json.loads('{"name": "Ada", "tags": ["math", "code"]}')
print(data["tags"][0])
print(json.dumps({"ok": True, "count": 2}))
try:
json.loads("{name: Ada}")
except json.JSONDecodeError as e:
print("bad JSON:", e.msg)math
{"ok": true, "count": 2}
bad JSON: Expecting property name enclosed in double quotes
- L3JSON objects become dicts, arrays become lists.
- L5Python True becomes JSON true.
{"ok": true, "count": 2} - L8Keys must be in double quotes in JSON, so this fails.
Parse, validate, create
Try it yourselfThe full path of a create request.
import json
USERS = []
def validate(data):
errors = []
if not isinstance(data.get("name"), str) or not data.get("name"):
errors.append("name is required")
if not isinstance(data.get("age"), int):
errors.append("age must be a whole number")
return errors
def create_user(body):
try:
data = json.loads(body)
except json.JSONDecodeError:
return 400, {"error": "invalid JSON"}
errors = validate(data)
if errors:
return 400, {"errors": errors}
user = {"id": len(USERS) + 1, "name": data["name"], "age": data["age"]}
USERS.append(user)
return 201, user
print(create_user('{"name": "Ada", "age": 36}'))
print(create_user('{"name": "", "age": "36"}'))
print(create_user('not json'))(201, {'id': 1, 'name': 'Ada', 'age': 36})
(400, {'errors': ['name is required', 'age must be a whole number']})
(400, {'error': 'invalid JSON'})
- L5Collect every problem, not just the first.
- L15Step 1: parse. Bad JSON stops here with 400.
- L18Step 2: validate.
- L22Step 3: store, with an id chosen by the server.
- L23Step 4: 201 and the new user.
Common mistakes
Stopping at the first error
If the client has three mistakes, reporting one at a time means three round trips. Collect them all.
Checking with `if data["age"]`
A missing key raises KeyError (a 500), and age 0 counts as false. Use data.get("age") and isinstance.
True is an int
In Python, isinstance(True, int) is True. If a field must be a real number, also reject bools.
Quick quiz
1. A client sends `{"qty": "three"}`, but qty must be a number. Which response fits?
Show the answer
Answer: A
The client made a mistake it can fix: 4xx, with a helpful message.
2. What does this print?
import json
print(json.loads('[1, "2", true, null]'))Show the answer
Answer: A
JSON true and null become Python True and None; "2" stays a string.
Practice
3 exercises in three steps: predict what a program prints, change a program to meet a new requirement, then write one from scratch. Your code runs in the browser and is checked the moment you submit.
- PredictThree requests
Type exactly what this prints.
- ModifyA stricter age check
validatelets bad ages through:-5,300andTrueall pass. Change it soagemust be anint(not a bool) from 0 to 150, with the error"age must be a whole number from 0 to 150". Keep the name check. - WriteCreate an order
Write
create_order(body), wherebodyis JSON text:- not valid JSON →
(400, {"error": "invalid JSON"}) - otherwise collect errors, in this order:
-
"item is required"unlessitemis a non-empty string
-"qty must be from 1 to 99"unlessqtyis an int (not a bool) from 1 to 99- any errors →
(400, {"errors": [...]}) - valid → append
{"id": ..., "item": ..., "qty": ...}toORDERS(ids start at 1) and return(201, order)
- not valid JSON →
Practise this lesson
Open the interactive lesson to edit and run these examples and solve the exercises with instant feedback. Free to start, no sign-up.
Interactive lessons open in order. If you have not done the earlier ones yet, you start at the course map.
Terms in this lesson
| Term | Chinese | Definition |
|---|---|---|
| json module | json 模块 | json.loads turns JSON text into Python data; json.dumps does the reverse. |
| Validation | 校验 | Checking input before trusting it. |