Python JSON schema

Stefan Alfbo - May 7 - - Dev Community

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.

curl command

We are now ready to add the jsonschema library to the project.



pip install jsonschema


Enter fullscreen mode Exit fullscreen mode

With that in place, lets create a folder where we can put our schemas in, call the folder, json-schema.



mkdir quickstart/json-schema


Enter fullscreen mode Exit fullscreen mode

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.



{}


Enter fullscreen mode Exit fullscreen mode

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"
}


Enter fullscreen mode Exit fullscreen mode

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": []
}


Enter fullscreen mode Exit fullscreen mode

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"
}


Enter fullscreen mode Exit fullscreen mode

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": {}
    }
  }
}


Enter fullscreen mode Exit fullscreen mode

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"]
}


Enter fullscreen mode Exit fullscreen mode

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


Enter fullscreen mode Exit fullscreen mode

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)


Enter fullscreen mode Exit fullscreen mode

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.

error

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!

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .