The payload still parses as JSON. The total is now a string. Syntax validation will accept it, but the downstream report may no longer receive the number it expects.

A JSON Schema makes the expected structure explicit. We will check one small order contract, classify six incoming records, and keep only the valid paid orders.

Explore the idea

Same field name. Different contract result.

Our contract requires a nonnegative number for total.

Fails

wrong JSON value type

Text is not a numeric total

A quoted number remains a JSON string. Valid JSON syntax does not mean the document follows the contract.

A contract small enough to inspect

Each order must contain an integer order_id, a nonnegative numeric total, and a status of paid or cancelled. Extra fields are allowed here: the schema does not set additionalProperties to false.

The order contract
{
  "type": "object",
  "required": [
    "order_id",
    "total",
    "status"
  ],
  "properties": {
    "order_id": {
      "type": "integer"
    },
    "total": {
      "type": "number",
      "minimum": 0
    },
    "status": {
      "type": "string",
      "enum": [
        "paid",
        "cancelled"
      ]
    }
  }
}

Download this schema to use the same contract locally.

incoming6 rows

Six incoming API records

The input is valid JSON or SQL NULL. The contract checks structure and values after JSON parsing.

idpayload
1{"order_id":101,"total":40,"status":"paid"}
2{"order_id":102,"total":15,"status":"cancelled"}
3{"order_id":103,"total":"40","status":"paid"}
4{"order_id":104,"total":-5,"status":"paid"}
5{"order_id":105,"total":10}
6NULL

The full schema lives in the one-row contract table in the setup. Notice the separation between properties, which describes fields, and required, which says they must exist. A property declaration alone does not make a field mandatory.

The browser loads Query.Farm’s JSON Schema extension for you. The downloaded walkthrough includes installation instructions for native DuckDB 2.0. The extension reference covers other supported versions and its draft-07 target.

Check the schema before checking the batch

Run the schema check on its own, outside any error-catching wrapper:

Check the contract separately

SELECT json_schema_validate_schema(definition) AS schema_ok FROM contract;
Expected result · sample data1 row
schema_ok
true

This verifies that the validator can load our contract. Also test it with known passing and failing documents: an accepted schema can still describe the wrong business rule.

This ordering matters because TRY catches errors in the schema as well as errors in a document. If you put a broken contract inside a blanket TRY, every incoming record could look like bad data when the real fault is the contract itself.

Turn document failures into a visible classification

The extension returns true for a conforming document and raises an error when validation fails. For a batch classification, catch that failure and map it to false:

Classify every incoming record

SELECT id, COALESCE(TRY(json_schema_validate(definition, payload)), false) AS valid
FROM incoming CROSS JOIN contract ORDER BY id;
Expected result · sample data6 rows
id valid
1 true
2 true
3 false
4 false
5 false
6 false

Records 1 and 2 pass. Record 3 has a string total, record 4 has a negative total, and record 5 lacks status. Our wrapper also treats record 6’s SQL NULL as invalid.

TRY does not repair any of these records. The original payload stays unchanged. This is a classification that lets a later query route or inspect records without abandoning the entire batch on the first failure.

The extension does not return a structured list of failed keywords. The original payload and the schema remain the evidence for investigation; run validation on an individual failing record when you want its textual exception. For this small fixture, the mismatches are visible directly in the input table.

Make the quality gate countable

A summary should account for the entire batch:

Summarize the quality gate

WITH checked AS (
  SELECT COALESCE(TRY(json_schema_validate(definition, payload)), false) AS valid
  FROM incoming CROSS JOIN contract
)
SELECT count(*) AS received, count(*) FILTER (WHERE valid) AS accepted,
       count(*) FILTER (WHERE NOT valid) AS rejected
FROM checked;
Expected result · sample data1 row
received accepted rejected
6 2 4

Six received, two accepted, four rejected. Persist a batch identifier and schema version alongside those outcomes in a recurring pipeline. That lets someone distinguish a changed source from a changed contract.

Validation and business filtering are separate steps. A cancelled order can be structurally valid. Conversely, the presence of the text paid does not prove that the rest of the document is acceptable.

Your turn · check your answer

Keep only valid paid orders

Return id for records that satisfy the contract and have status paid. A paid label alone is not enough.

See the expected result
Your target result1 row
id
1
Sample data is loaded for you.

Preparing the editor…

Need a hint?

Combine the validation predicate with the status predicate. Check the contract separately before using TRY on records.

Show a solution
SELECT id FROM incoming CROSS JOIN contract
WHERE COALESCE(TRY(json_schema_validate(definition, payload)), false)
  AND payload ->> '$.status' = 'paid'
ORDER BY id;

Where this check belongs

This lesson starts with parsed JSON. Malformed JSON text fails earlier, before the contract can inspect its fields; retain parsing failures as a separate category in an import workflow.

For fixed relational data, typed columns and SQL constraints may be a simpler way to express requirements. JSON Schema is useful when a JSON document is the thing whose structure you need to check. Validate it once at the boundary where the result becomes useful, rather than repeatedly on every dashboard query.

Once the contract passes, flatten the nested data using a deliberate rule for empty arrays. Keep the raw payload available until the resulting tables have been reconciled with the input.