{ "$schema": "https://json-schema.org/draft/2020-12/schema", "$id": "https://openpredicate.tech/schema/v0.4.0/open-predicate-schema.json", "title": "OpenPredicate — Filter", "description": "A JSON-encoded, SQL-flavoured predicate language for describing the request body of search operations (POST /…/search and QUERY /…). Everything needed to write a correct filter is described here, operator by operator, so one grammar can be shared by every search endpoint in an API: reference this schema from an OpenAPI document, or inline it as the input schema of a search tool. Normative semantics are defined in SPEC.md.", "$comment": "Version 0.4.0. See https://github.com/OpenPredicate/open-predicate", "$ref": "#/$defs/Filter", "x-profiles": { "core": ["$and", "$or", "$nor", "$not", "$eq", "$ne", "$in", "$nin", "$gt", "$gte", "$lt", "$lte", "$exists", "$isNull", "$unknownAs"], "strings": ["$like", "$nlike", "$ilike", "$nilike", "$startsWith", "$endsWith", "$contains"], "regex": ["$regex", "$flags"], "ranges": ["$between", "$nbetween"], "types": ["$type"], "collections": ["$some", "$every", "$hasAll", "$size"], "refs": ["$field", "$literal"], "text": ["$search"] }, "$defs": { "Filter": { "title": "Filter", "description": "A predicate over a record. Sibling members are combined with implicit AND: every member must evaluate TRUE for the filter to match. Keys beginning with '$' are reserved for logical operators; a field whose real name begins with '$' is escaped by doubling it ('$$price' means the field '$price').", "type": "object", "minProperties": 1, "properties": { "$and": { "title": "$and", "description": "All of the listed filters must evaluate TRUE.", "type": "array", "minItems": 1, "items": { "$ref": "#/$defs/Filter" } }, "$or": { "title": "$or", "description": "At least one of the listed filters must evaluate TRUE.", "type": "array", "minItems": 1, "items": { "$ref": "#/$defs/Filter" } }, "$nor": { "title": "$nor", "description": "NOT (a OR b OR …), evaluated under three-valued logic. It is TRUE only when every listed filter is FALSE: if one of them is UNKNOWN the result is UNKNOWN, not TRUE, so a $nor over a nullable field excludes the records whose field is null or absent. See SPEC.md §4.1.", "type": "array", "minItems": 1, "items": { "$ref": "#/$defs/Filter" } }, "$not": { "title": "$not", "description": "Negates a filter. Under three-valued logic NOT UNKNOWN is UNKNOWN, not TRUE — so this does not match records whose field is null or absent. Add $unknownAs to the inner constraint to include them. See SPEC.md §4.1.", "$ref": "#/$defs/Filter" } }, "patternProperties": { "^(?:[^$]|\\$\\$)": { "$ref": "#/$defs/Constraint" } }, "propertyNames": { "$comment": "Deliberately overlaps with patternProperties/additionalProperties above: those two enforce the default grammar, while this is the single named override point. Narrow #/$defs/FieldPath in a bundled copy of this schema to restrict an endpoint to a fixed field set — the narrowing then applies at every nesting level, because Filter recurses through this same $ref. See README §Restricting the queryable field set.", "anyOf": [ { "enum": ["$and", "$or", "$nor", "$not"] }, { "$ref": "#/$defs/FieldPath" } ] }, "additionalProperties": false, "examples": [ { "status": "open" }, { "department": { "$eq": "sales" }, "age": { "$gte": 18 } }, { "$and": [ { "department": { "$eq": "sales" } }, { "$or": [{ "role": "admin" }, { "role": "manager" }] } ] } ] }, "FieldPath": { "title": "Field path", "description": "A dotted path to a member of the record: 'a.b.c'. An array element is addressed by index ('items[0]'); to say something about the elements of an array without naming an index, use $some or $every, which quantify explicitly. A literal '.' inside a key is escaped as '\\.'. The default rule is permissive — any non-empty string not beginning with a single '$' — because the set of queryable paths is a property of the resource, not of the language. Full grammar in SPEC.md §3.", "type": "string", "minLength": 1, "pattern": "^(?:[^$]|\\$\\$)", "not": { "$comment": "The '[*]' wildcard was removed in v0.4.0; $some and $every replace it. Rejecting it here turns a stale filter into a validation error instead of a path that would be read as a literal key name.", "pattern": "\\[\\*\\]" }, "examples": ["name", "address.city", "items[0].sku", "$$internal"] }, "Constraint": { "title": "Constraint", "description": "The condition applied to one field. Either a scalar shorthand — {\"status\": \"open\"} is exactly {\"status\": {\"$eq\": \"open\"}} — or an object of operators. Arrays and objects are deliberately excluded from the shorthand so that {\"tags\": [\"a\"]} can never be read ambiguously as either $eq or $in.", "anyOf": [ { "$ref": "#/$defs/ScalarOrNull" }, { "$ref": "#/$defs/ConstraintObject" } ] }, "ConstraintObject": { "title": "Constraint object", "description": "One or more operators applied to the same field. Sibling operators are combined with implicit AND, so {\"age\": {\"$gt\": 18, \"$ne\": 30}} is a single well-formed constraint.", "type": "object", "minProperties": 1, "dependentSchemas": { "$unknownAs": { "$comment": "$unknownAs is a modifier, not a predicate: it needs at least one operator to modify. Requiring a second member forbids {\"$unknownAs\": true} standing alone.", "minProperties": 2 } }, "properties": { "$eq": { "title": "$eq", "description": "Field equals the operand. Any JSON value, including null, arrays and objects. A value of a different JSON type is unequal — FALSE, not UNKNOWN.", "$ref": "#/$defs/Operand" }, "$ne": { "title": "$ne", "description": "Field does not equal the operand. The negation of $eq under three-valued logic, so this is UNKNOWN — and therefore does not match — when the field is null or absent. Add $unknownAs: true to include those records. See SPEC.md §4.1.", "$ref": "#/$defs/Operand" }, "$gt": { "title": "$gt", "description": "Field is strictly greater than the operand.", "$ref": "#/$defs/OrderedOperand" }, "$gte": { "title": "$gte", "description": "Field is greater than or equal to the operand.", "$ref": "#/$defs/OrderedOperand" }, "$lt": { "title": "$lt", "description": "Field is strictly less than the operand.", "$ref": "#/$defs/OrderedOperand" }, "$lte": { "title": "$lte", "description": "Field is less than or equal to the operand.", "$ref": "#/$defs/OrderedOperand" }, "$between": { "title": "$between", "description": "Field falls within [lower, upper], inclusive of both bounds.", "$ref": "#/$defs/Bounds" }, "$nbetween": { "title": "$nbetween", "description": "Field falls outside [lower, upper]. The negation of $between under three-valued logic, so it is UNKNOWN when the field is null or absent.", "$ref": "#/$defs/Bounds" }, "$in": { "title": "$in", "description": "Field equals at least one member of the list. Compares the whole value — it does NOT test membership inside an array-valued field: {\"tags\": {\"$in\": [\"a\"]}} asks whether tags equals \"a\". For array elements quantify explicitly: {\"tags\": {\"$some\": {\"$in\": [\"a\"]}}}.", "$ref": "#/$defs/OperandSet" }, "$nin": { "title": "$nin", "description": "Field equals no member of the list. Like $in, compares the whole value rather than array elements. The negation of $in under three-valued logic, so it is UNKNOWN when the field is null or absent.", "$ref": "#/$defs/OperandSet" }, "$like": { "title": "$like", "description": "SQL LIKE pattern: '%' matches any run of characters, '_' matches exactly one, and '\\' escapes either. Case-sensitive.", "type": "string", "examples": ["%urgent%", "INV-____", "100\\%"] }, "$nlike": { "title": "$nlike", "description": "Negated $like, under three-valued logic: UNKNOWN when the field is null, absent or not a string.", "type": "string" }, "$ilike": { "title": "$ilike", "description": "Case-insensitive $like. Collation is server-defined; see SPEC.md §5.5.", "type": "string" }, "$nilike": { "title": "$nilike", "description": "Negated $ilike, under three-valued logic: UNKNOWN when the field is null, absent or not a string.", "type": "string" }, "$startsWith": { "title": "$startsWith", "description": "Field begins with this literal substring. Wildcards are not interpreted.", "type": "string" }, "$endsWith": { "title": "$endsWith", "description": "Field ends with this literal substring. Wildcards are not interpreted.", "type": "string" }, "$contains": { "title": "$contains", "description": "Field contains this literal substring. String-only — for arrays, quantify over the elements with $some or $every.", "type": "string" }, "$regex": { "title": "$regex", "description": "Field matches this regular expression (ECMA-262 syntax). Servers SHOULD bound execution time; see SPEC.md §7.", "type": "string", "format": "regex" }, "$flags": { "title": "$flags", "description": "Modifiers for $regex: 'i' case-insensitive, 'm' multiline, 's' dot-matches-newline. Only valid alongside $regex.", "type": "string", "pattern": "^[ims]{0,3}$" }, "$exists": { "title": "$exists", "description": "true if the field is present on the record, even when its value is null; false if absent. Total — never UNKNOWN. Distinct from $isNull — see SPEC.md §4.2.", "type": "boolean" }, "$isNull": { "title": "$isNull", "description": "true if the field's value is null (equivalent to {\"$eq\": null}); false if it is non-null (equivalent to {\"$ne\": null}). UNKNOWN only when the path resolves to nothing.", "type": "boolean" }, "$type": { "title": "$type", "description": "Field's JSON type. 'integer' matches a number with no fractional part.", "type": "string", "enum": ["string", "number", "integer", "boolean", "object", "array", "null"] }, "$some": { "title": "$some", "description": "At least one element of the array-valued field satisfies this condition. Supply a Filter for arrays of objects (paths are relative to the element) or a constraint object for arrays of scalars. An element for which the condition is UNKNOWN does not satisfy it, so the quantifier yields TRUE or FALSE over any array — FALSE for an empty one. It is UNKNOWN only when the field is not an array, including when it is null or absent. See SPEC.md §5.8.", "anyOf": [ { "$ref": "#/$defs/Filter" }, { "$ref": "#/$defs/ConstraintObject" } ], "examples": [{ "qty": { "$gt": 2 } }, { "$in": ["urgent", "p1"] }] }, "$every": { "title": "$every", "description": "Every element of the array-valued field satisfies this condition; an element for which it is UNKNOWN does not. TRUE for an empty array, vacuously. UNKNOWN only when the field is not an array, including when it is null or absent — a missing array is not vacuously TRUE. Same operand shape as $some. See SPEC.md §5.8.", "anyOf": [ { "$ref": "#/$defs/Filter" }, { "$ref": "#/$defs/ConstraintObject" } ], "examples": [{ "status": "settled" }, { "$gte": 0 }] }, "$hasAll": { "title": "$hasAll", "description": "The array-valued field contains every member of this list. Quantifies over the operand rather than over the elements, which is why it is not expressible as a $some or $every. Set semantics: multiplicity is ignored. UNKNOWN when the field is not an array.", "$ref": "#/$defs/OperandSet" }, "$size": { "title": "$size", "description": "Length of the array-valued field: either an exact count or a nested numeric constraint.", "$ref": "#/$defs/SizeConstraint" }, "$search": { "title": "$search", "description": "Free-text match against the field. Tokenisation, stemming and relevance are server-defined.", "type": "string", "minLength": 1 }, "$not": { "title": "$not (field-level)", "description": "Negates the constraint on this field. {\"age\": {\"$not\": {\"$gt\": 5}}} is TRUE when age is not greater than 5 and UNKNOWN when age is null or absent.", "$ref": "#/$defs/ConstraintObject" }, "$unknownAs": { "title": "$unknownAs", "description": "Resolves UNKNOWN to TRUE or FALSE for this constraint, applied last — after every sibling operator, including a field-level $not. {\"status\": {\"$ne\": \"archived\", \"$unknownAs\": true}} means 'not archived, and count the records where status is null or absent'. A modifier, not a predicate: it needs at least one operator beside it. No-op on $exists, which is already total. See SPEC.md §4.6.", "type": "boolean" } }, "dependentRequired": { "$flags": ["$regex"] }, "additionalProperties": false, "examples": [ { "$eq": "open" }, { "$gt": 18, "$ne": 30 }, { "$in": ["red", "green"] }, { "$regex": "^inv-", "$flags": "i" }, { "$ne": "archived", "$unknownAs": true }, { "$some": { "$in": ["urgent", "p1"] } } ] }, "ScalarOrNull": { "title": "Scalar value", "description": "A JSON scalar or null.", "type": ["string", "number", "boolean", "null"] }, "Operand": { "title": "Operand", "description": "A value to compare against: any JSON value, a reference to another field of the same record, or a $literal-wrapped value.", "anyOf": [ { "$ref": "#/$defs/ValueRef" }, { "$ref": "#/$defs/LiteralWrapper" }, { "$ref": "#/$defs/PlainValue" } ] }, "OrderedOperand": { "title": "Ordered operand", "description": "A value on which an ordering is defined: a number, or a string compared lexicographically — which for RFC 3339 date, date-time and time strings coincides with chronological order.", "anyOf": [ { "type": "number" }, { "type": "string" }, { "type": "string", "format": "date" }, { "type": "string", "format": "date-time" }, { "type": "string", "format": "time" }, { "$ref": "#/$defs/ValueRef" } ], "examples": [50, "2023-01-01", "2023-01-01T00:00:00Z", { "$field": "cost" }] }, "ValueRef": { "title": "Field reference", "description": "Compares against another field of the same record rather than a constant: {\"price\": {\"$gt\": {\"$field\": \"cost\"}}} is SQL's WHERE price > cost.", "type": "object", "required": ["$field"], "properties": { "$field": { "$ref": "#/$defs/FieldPath" } }, "additionalProperties": false }, "LiteralWrapper": { "title": "Literal escape", "description": "Forces the wrapped value to be treated as data. Required only when an object operand would otherwise look like a field reference, e.g. {\"$literal\": {\"$field\": \"not-a-reference\"}}.", "type": "object", "required": ["$literal"], "properties": { "$literal": true }, "additionalProperties": false }, "PlainValue": { "title": "Plain value", "description": "Any JSON value that is not an object carrying '$'-prefixed keys. The '$' prefix is reserved for $field and $literal, so plain object operands must not use it.", "if": { "type": "object" }, "then": { "type": "object", "propertyNames": { "pattern": "^[^$]" } } }, "Bounds": { "title": "Bounds", "description": "An inclusive [lower, upper] pair. Both bounds SHOULD be of the same type.", "type": "array", "minItems": 2, "maxItems": 2, "items": { "$ref": "#/$defs/OrderedOperand" }, "examples": [[10, 20], ["2023-01-01", "2023-12-31"]] }, "OperandSet": { "title": "Operand set", "description": "A non-empty list of distinct values. Servers MAY cap its length; see SPEC.md §7.", "type": "array", "minItems": 1, "uniqueItems": true, "items": { "$ref": "#/$defs/Operand" }, "examples": [["red", "green", "blue"], [1, 2, 3]] }, "SizeConstraint": { "title": "Size constraint", "description": "An exact array length, or a comparison on that length.", "anyOf": [ { "type": "integer", "minimum": 0 }, { "type": "object", "minProperties": 1, "properties": { "$eq": { "type": "integer", "minimum": 0 }, "$ne": { "type": "integer", "minimum": 0 }, "$gt": { "type": "integer", "minimum": 0 }, "$gte": { "type": "integer", "minimum": 0 }, "$lt": { "type": "integer", "minimum": 0 }, "$lte": { "type": "integer", "minimum": 0 } }, "additionalProperties": false } ], "examples": [3, { "$gte": 1 }] } } }