If you have a web API you're probably using JSON as the format to exchange data between the API and the client.
This exchange forms an implicit contract between the API and its consumers.
JSON Schema can be a great tool to document this contract, define constraints and validate the contract.
In Python we can use the jsonschema library to enable the power of JSON Schema in our projects.
Lets give it a try based on the quickstart project from Django REST framework page. Make sure that the test with the curl command is working as expected before moving on.
We are now ready to add the jsonschema
library to the project.
pip install jsonschema
With that in place, lets create a folder where we can put our schemas in, call the folder, json-schema
.
mkdir quickstart/json-schema
Next step is to add a schema file for the /users/{id}
endpoint, we will call this file, user.json
, and place the file in the json-schema
folder.
The schema is defined as JSON, so we start with an empty schema.
{}
A good practice is to start with a declaration of which JSON Schema we are using, and that is done with a simple keyword property, $schema
.
{
"$schema": "https://json-schema.org/draft/2020-12/schema"
}
The type that the /users/{id}
endpoint is returning is an object, which looks like this.
{
"url": "http://127.0.0.1:8000/users/2/?format=json",
"username": "tester",
"email": "tester@example.com",
"groups": []
}
Therefore we will add a type
property to our schema to say that we are returning an object
.
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object"
}
Next up is to define the properties of that object.
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"properties": {
"url": {
"type": "string",
"format": "uri"
},
"username": {
"type": "string"
},
"email": {
"type": "string",
"format": "email"
},
"groups": {
"type": "array",
"items": {}
}
}
}
So the first property of the user object is, url
, which is of type string
, but it should conform to the uri
format. The username
is just a string, email
is also of the type string but has the format email
. Finally the last property, groups
, is of the type array
and the items
defines how each element in that array should look like. However, in this case we don't really know the type, so we use {}
to indicate any type.
As you can see it's pretty straightforward to define a json-schema for JSON data.
We could also add which fields are required in the JSON response, which is done with property keyword required
.
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"properties": {
"url": {
"type": "string",
"format": "uri"
},
"username": {
"type": "string"
},
"email": {
"type": "string",
"format": "email"
},
"groups": {
"type": "array",
"items": {}
}
},
"required": ["url", "username", "email", "groups"]
}
Ok, now we require that all fields need to be included in the response. The json-schema is complete and it's time to do some testing of the endpoint. We begin with adding a new file for tests, views_test.py
, and add this content to the test file.
import pytest
from rest_framework.test import APIClient
from django.contrib.auth.models import User
@pytest.mark.django_db
def test_get_user():
# Create a user
_ = User.objects.create_user(username="test-user", email="test-user@example.com")
client = APIClient()
response = client.get('/users/1/')
assert response.status_code == 200
I have changed one thing in the UserViewSet
to get this to work, and that is to change the permission_classes
to permission.AllowAny
, just for demo purposes.
We can now extend our test and use the jsonschema
library. In the example below is a test that is validating that the response from our API is fulfilling our contract (JSON schema) by using the validate
function.
import json
from pathlib import Path
import jsonschema
import pytest
from rest_framework.test import APIClient
from django.contrib.auth.models import User
@pytest.mark.django_db
def test_get_user():
# Create a user
_ = User.objects.create_user(username="test-user", email="test-user@example.com")
client = APIClient()
response = client.get('/users/1/')
assert response.status_code == 200
# Load the JSON schema
user_json_schema = Path(".") / "quickstart" / "json-schema" / "user.json"
with open(user_json_schema) as schema_file:
schema = json.load(schema_file)
# Validate the response JSON against the schema
jsonschema.validate(response.json(), schema)
When running the test now it will validate that the json response is consistent to our schema definition. Try to change the schema so that the test fails. The image below is showing the case when I have changed the type of the username
from string
to number
.
As you can see this tool can make you more certain that your API stays true to its contract and you will have documentation that can be shared with the consumers of the API.
The code above is just a simple example to get started with jsonschema
, however go and check out the docs and learn/play more with it.
Happy validating!