Python JSON schema
Tags: 100daystooffload, json, django, python
Published: 2024-05-07 19:57:21

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](https://json-schema.org/) can be a great tool to document this contract, define constraints and validate the contract.

In Python we can use the [jsonschema](https://pypi.org/project/jsonschema/) library to enable the power of JSON Schema in our projects.

Lets give it a try based on the [quickstart project](https://www.django-rest-framework.org/tutorial/quickstart/) from Django REST framework page. Make sure that the test with the curl command is working as expected before moving on.

![curl command](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/56rhu3156954v24mln6y.png)

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

```terminal
pip install jsonschema
```

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

```terminal
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.

```json
{}
```

A good practice is to start with a declaration of which JSON [Schema](https://json-schema.org/understanding-json-schema/basics#declaring-a-json-schema) we are using, and that is done with a simple keyword property, `$schema`.

```json
{ 
  "$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.

```json
{
  "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`.

```json
{ 
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "type": "object"
}
```

Next up is to define the properties of that object.

```json
{
  "$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`.

```json
{
  "$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.

```python
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.

```python
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`.

![error](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/wk2o7darnoj1e4evq2c4.png)

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](https://python-jsonschema.readthedocs.io/en/latest/) and learn/play more with it.

Happy validating!