Predicates, as JSON.
Described by a schema.

OpenPredicate is an open standard for the filter half of a search API — a JSON-encoded, SQL-flavoured predicate language defined by a single JSON Schema. Write the grammar once, and every POST /…/search, every QUERY /… and every search tool you hand an agent speaks it.

  • JSON Schema draft 2020-12
  • Grammar v0.4.0
  • MIT licensed
  • No dependencies
Available cats and dogs, or any rescue, born since 2020
{
  "$and": [
    { "status": "available" },
    { "$or": [
        { "species": { "$in": ["cat", "dog"] } },
        { "tags":    { "$some": { "$in": ["rescue", "senior"] } } }
    ]},
    { "born": { "$gte": "2020-01-01" } }
  ]
}

Why a standard for this at all

Search endpoints attract bespoke query syntaxes. Each arrives as an opaque string — ?q=status:open AND born>2020 — that no schema can validate, no generator can type, and no client can build safely. Structuring the predicate as JSON changes what the rest of your stack can do with it.

One grammar, every endpoint

The schema covers the predicate and nothing else — no projection, ordering or pagination. That restraint is what makes it reusable: those parts differ per API, the filter does not. Clients learn one language instead of one syntax per endpoint.

Validated where APIs are already described

JSON Schema is the interchange format of OpenAPI 3.1 and it is what an MCP inputSchema is. So a filter validates in CI, shows up in generated docs, and becomes a real type in a generated client — with no new toolchain.

Servers say what they cannot do

No server implements every operator. A server advertises profiles and publishes a capability document, so a client learns the limits from a schema rather than from a runtime error.

The two rules worth learning first

Most of the language is guessable. These two are not, and between them they account for nearly every surprise. They are normative — the spec pins them down in §4.

1 Siblings mean AND. A bare value means equals.

At every level, sibling members are combined with implicit AND, and a scalar in the value position is shorthand for $eq. The two filters on the right are the same filter. You only need $and when you want the same field twice, or when you are nesting it inside $or.

What you write
{
  "status": "available",
  "species": "cat"
}
What it means
{
  "$and": [
    { "status": { "$eq": "available" } },
    { "species": { "$eq": "cat" } }
  ]
}

2 Comparison is three-valued, and only TRUE matches.

A comparison against a field that is null or absent is neither TRUE nor FALSE — it is UNKNOWN, and only TRUE matches. So a negation does not sweep up the records that have no value at all, which is the SQL behaviour and almost never what someone expects the first time.

When you do want them, say so with $unknownAs. Making it explicit is the whole point: the filter records the decision instead of leaving it to the server.

Only TRUE matches
Field{"$ne": "archived"}Matches?
"open"TRUEyes
"archived"FALSEno
nullUNKNOWNno
absentUNKNOWNno
Resolving UNKNOWN explicitly
// Does NOT match a pet whose status is absent or null
{ "status": { "$ne": "archived" } }

// Does match them: UNKNOWN is resolved to TRUE
{ "status": { "$ne": "archived", "$unknownAs": true } }

Two integration points, one schema

Because the grammar is a JSON Schema, integrating it is a $ref. Both of the places an API is described today already speak the format.

From an OpenAPI document

Reference the schema from the request body. It validates in CI and types your clients.

OpenAPI 3.1 · POST /pets/search
paths:
  /pets/search:
    post:
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                filter:
                  $ref: 'https://openpredicate.tech/schema/v0.4.0/open-predicate-schema.json'
                limit:
                  type: integer

As an MCP tool's inputSchema

The grammar becomes the contract a model writes filters against, with each operator's description carried along as the instructions.

MCP tool definition
{
  "name": "search_pets",
  "description": "Search the pet catalogue.",
  "inputSchema": {
    "type": "object",
    "properties": {
      "filter": { "$ref": "https://openpredicate.tech/schema/v0.4.0/open-predicate-schema.json" }
    }
  }
}

Or as the body of an HTTP QUERY

RFC 10008 gives search a safe, cacheable method with a body. A predicate is exactly what that body needs to be.

OpenAPI 3.2 · QUERY /pets
QUERY /pets HTTP/1.1
Host: api.example.com
Content-Type: application/json

{ "filter": { "status": "available" } }

And the server declares its slice

A capability document states the profiles served and each field's domain, so a client can narrow its filters before it sends one.

GET /pets/capabilities
{
  "queryLanguage": "https://openpredicate.tech/schema/v0.4.0/open-predicate-schema.json",
  "profiles": ["core", "strings", "ranges", "collections"],
  "fields": {
    "status": { "values": ["available", "pending", "sold"] },
    "born":   { "type": "string", "format": "date" }
  }
}

Start where you are

Honest status

OpenPredicate is pre-1.0. The grammar, the operator set and profile grouping, the null and three-valued semantics and the error model are stable enough to review and to build against — they are what the schema, the spec and the test suite pin down. The grammar may still change before 1.0, and every break is recorded in the changelog with a migration note.

Not yet true: the packages are not published to a registry, and this site is the schema's first resolvable home. Pin the versioned $id, or vendor the file.

Stewarded by the OpenPredicate organisation, whose purpose is to take this grammar to an open standard and push for its adoption. Disagreement is the most useful contribution at this stage — open an issue.