# Schema Validation

Schema validation involves validating some payload against a defined schema. This usually means PUT and POST HTTP requests.

Let's say we define a schema for a Profile as such:

```javascript
{
  "definitions": {},
  "$schema": "http://json-schema.org/draft-07/schema#",
  "type": "object",
  "title": "POST Profile schema",
  "required": [
    "profile_id",
    "profile_name",
    "profile_type",
    "roles"
  ],
  "properties": {
    "profile_id": {
      "$id": "/properties/profile_id",
      "type": "integer",
      "title": "Id for this profile"
    },
    "profile_name": {
      "$id": "/properties/profile_name",
      "type": "string",
      "title": "The profile name",
      "examples": [
        "BTS"
      ],
      "pattern": "^(.*)$"
    },
    "profile_type": {
      "$id": "/properties/profile_type",
      "type": "string",
      "title": "The profile type",
      "examples": [
        "ArtistProfile",
        "LabelManager"
      ]
    },
    "roles": {
      "$id": "/properties/roles",
      "type": "array",
      "title": "Roles allowed for this profile",
      "examples": [
        [
          "analytics",
          "accounting"
        ],
        [
          "analytics"
        ]
      ]
    }
  }
}
```

And the payload from a POST request is:

```javascript
{
    "profile_name": 9999999
    "profile_type": "banana",
    "roles": "pizza"
}
```

We want to return a `400` with code `validation_error` and also let the caller know the following:

* `profile_id` is missing but required
* `profile_name` must be a string
* `profile_type` is not in the allowed types \(LabelProfile, ArtistProfile, etc\)
* `roles` must be an array

## Schema Validation: Python

In python [marshmallow](https://marshmallow.readthedocs.io/en/stable/) is preferred for schema definition due to it's declarative nature and descriptive error responses. This combined with a route decorator provides a simple recipe for schema validation.

### Schema Definition

In your microservice \(we will be using `ows-users` in this documentation\), place your schema definitions somewhere like `/users/validation/schemas/*`.

This is the definition for a Profile schema using the marshmallow framework:

```python
"""Profile schema."""
from enum import Enum
from marshmallow import Schema, fields
from marshmallow_enum import EnumField


class ProfileType(Enum):
    ArtistProfile = 'ArtistProfile'
    LabelProfile = 'LabelProfile'


class Profile(Schema):
    """ Profile schema.
    Properties:
     - profile_id (str): the id for this profile
        - LabelProfile = vend_contact_id
        - ArtistProfile = auto-id
     - profile_name (str): name for the profile
     - profile_type (str): the type of profile 
        (e.g. 'ArtistProfile', 'LabelProfile')
     - roles (list): list of role strings
    """
    profile_id = fields.Integer(required=True)
    profile_name = fields.String(required=True)
    profile_type = EnumField(ProfileType, required=True)
    roles = fields.List(fields.String(), required=True)
```

Property types, max values / ranges, required and other options can be defined for each field. Here, a custom enum has been set for the `profile_type` property.

### Route Decoration

The way to use the schema definition is by the [`validate_request_data()`](https://github.com/theorchard/ows-users/blob/master/users/utils/api_utils.py#L15) Flask route decorator defined in `users/utils/api_utils.py`.

> Note: `api_utils` will be moved to a shared pypi library.

Now, we can decorate the route in the handler, passing the schema we want to use to validate the POST body payload, `ProfileSchema()`.

```python
"""handlers.py"""
...
from users.utils.api_utils import validate_request_data
...
from users.validation.schemas.profile import Profile as ProfileSchema

...

@app.route(
    '/profile/identity/<orchard_identity_id>', methods=['POST'])
@validate_request_data(ProfileSchema())
def create_profile_for_identity(orchard_identity_id):
...
```

Now, given the following JSON payload in `request.get_json()`...

```javascript
{
    "profile_name": 9999999,
    "profile_type": "banana",
    "roles": "pizza"
}
```

This will be the Flask response, before even getting to the logic layer:

**400 Bad Request**

```javascript
{
    "code": "validation_error",
    "message": {
        "roles": [
            "Not a valid list."
        ],
        "profile_name": [
            "Not a valid string."
        ],
        "profile_type": [
            "Invalid enum member banana"
        ],
        "profile_id": [
            "Missing data for required field."
        ]
    }
}
```

