237 lines
4.8 KiB
Markdown
237 lines
4.8 KiB
Markdown
---
|
||
tags:
|
||
- json
|
||
- supabase
|
||
---
|
||
# JSON Schema validation for Postgres
|
||
|
||
When using the JSON (or JSONB) datatype, the data needs to be validated to assure database integrity.
|
||
|
||
pg_jsonschema is a PostgreSQL extension for SupaBase that can validate `json` and `jsonb` data types against a JSON Schema. The extension offers two functions:
|
||
|
||
|
||
```java
|
||
-- Validates a json *instance* against a JSON Schema *schema*
|
||
json_matches_schema(schema json, instance json) returns bool
|
||
|
||
-- Validates a jsonb *instance* against a JSON Schema *schema*
|
||
jsonb_matches_schema(schema json, instance jsonb) returns bool
|
||
```
|
||
|
||
JSON Schema is a way to define what valid JSON should look like for a particular use case:
|
||
|
||
- What properties an object should have
|
||
- What data types are expected
|
||
- Which fields are required vs optional
|
||
- Validation constraints (like minimum/maximum values, string patterns, etc.)
|
||
- Default values and descriptions
|
||
|
||
A JSON Schema is itself a JSON document. Here's a simple example of a JSON schema:
|
||
|
||
```json
|
||
{
|
||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||
"type": "object",
|
||
"properties": {
|
||
"name": {
|
||
"type": "string",
|
||
"description": "Person's full name"
|
||
},
|
||
"age": {
|
||
"type": "integer",
|
||
"minimum": 0,
|
||
"maximum": 150
|
||
},
|
||
"email": {
|
||
"type": "string",
|
||
"format": "email"
|
||
}
|
||
},
|
||
"required": ["name", "email"]
|
||
}
|
||
```
|
||
|
||
You can check input against a schema in SQL like this:
|
||
|
||
```sql
|
||
create table some_table(
|
||
id serial primary key, -- db-column `id` column is an auto-incrementing primary key
|
||
metadata json not null, -- db-column `metadata` must contain a JSON value and cannot be null
|
||
check ( -- table-level check constraint to match the JSON in `metadata` to the schema
|
||
json_matches_schema(
|
||
schema :='{
|
||
"type": "object", -- we require an object ...
|
||
"properties": {
|
||
"foo": { -- with a single string property `"foo"` ...
|
||
"type": "string"
|
||
}
|
||
},
|
||
"required": ["foo"], -- property `"foo"` is required ...
|
||
"additionalProperties": false -- and no additional properties are allowed
|
||
}',
|
||
instance := metadata -- the value of the `metadata` column is passed ...
|
||
-- as the `instance` argument to the `json_matches_schema` function, for each row
|
||
)
|
||
)
|
||
);
|
||
|
||
|
||
-- Now we can attempt to insert a row into `some_table`,
|
||
-- with the `metadata` value provided as `<SQL input>`
|
||
|
||
insert into some_table(metadata)
|
||
values
|
||
(<SQL input>);
|
||
|
||
-- <SQL input> needs to be replaced with an actual JSON value, e.g. '{"foo": "bar"}'.
|
||
-- The insert will only succeed if the contents of `metadata` matches the schema in the check constraint.
|
||
```
|
||
|
||
## Validating for a set of allowed values
|
||
|
||
Use the `enum` keyword to validate that a value must be one of a specific set of allowed values.
|
||
|
||
**String values:**
|
||
|
||
```json
|
||
{
|
||
"type": "string",
|
||
"enum": ["red", "green", "blue"]
|
||
}
|
||
```
|
||
|
||
**Mixed data types:**
|
||
|
||
```json
|
||
{
|
||
"enum": ["active", "inactive", null, 42]
|
||
}
|
||
```
|
||
|
||
**In an object property:**
|
||
|
||
```json
|
||
{
|
||
"type": "object",
|
||
"properties": {
|
||
"status": {
|
||
"type": "string",
|
||
"enum": ["pending", "approved", "rejected"]
|
||
},
|
||
"priority": {
|
||
"type": "integer",
|
||
"enum": [1, 2, 3, 4, 5]
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
|
||
## Validating for a data range
|
||
|
||
**Inclusive bounds (default):**
|
||
|
||
```json
|
||
{
|
||
"type": "integer",
|
||
"minimum": 1,
|
||
"maximum": 10
|
||
}
|
||
```
|
||
|
||
This allows values from 1 to 10, including 1 and 10.
|
||
|
||
**Exclusive bounds:**
|
||
|
||
```json
|
||
{
|
||
"type": "number",
|
||
"exclusiveMinimum": 0,
|
||
"exclusiveMaximum": 100
|
||
}
|
||
```
|
||
|
||
This allows values greater than 0 and less than 100, but not 0 or 100 themselves.
|
||
|
||
**Mixed Bounds:**
|
||
|
||
```json
|
||
{
|
||
"type": "number",
|
||
"minimum": 0,
|
||
"exclusiveMaximum": 1
|
||
}
|
||
```
|
||
|
||
This allows values from 0 (inclusive) to 1 (exclusive), so 0 ≤ value < 1.
|
||
|
||
**One-Sided Ranges:**
|
||
|
||
```json
|
||
{
|
||
"type": "integer",
|
||
"minimum": 18
|
||
}
|
||
```
|
||
|
||
```json
|
||
{
|
||
"type": "number",
|
||
"maximum": 3.14159
|
||
}
|
||
```
|
||
|
||
|
||
**In Object Properties:**
|
||
|
||
```json
|
||
{
|
||
"type": "object",
|
||
"properties": {
|
||
"age": {
|
||
"type": "integer",
|
||
"minimum": 0,
|
||
"maximum": 150
|
||
},
|
||
"temperature": {
|
||
"type": "number",
|
||
"minimum": -273.15,
|
||
"maximum": 1000.0
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
|
||
**Regex Validation:**
|
||
|
||
```json
|
||
{
|
||
"type": "string",
|
||
"pattern": "^[a-zA-Z0-9]+$"
|
||
}
|
||
```
|
||
|
||
**Date Validation:**
|
||
|
||
JSON Schema supports the ISO 8601 date format:
|
||
|
||
```json
|
||
{
|
||
"type": "string",
|
||
"format": "date"
|
||
}
|
||
```
|
||
|
||
`"date"` validates dates like: `2023-12-25`
|
||
`"date-time"` validates like: `2023-12-25T10:30:00Z` or `2023-12-25T10:30:00.123Z`
|
||
`"time"` validates like: `10:30:00` or `10:30:00.123`
|
||
|
||
Using the ISO 8601 date format is recommended for interoperability.
|
||
Custom date patterns can be validated with Regex.
|
||
|
||
Ranges can be validated using the `"minimum"` and `"maximum"` keywords like before.
|
||
|
||
## Documentation**
|
||
- [pg_jsonschema](https://github.com/supabase/pg_jsonschema)
|
||
- [JSON Schema](https://json-schema.org/)
|