> For clean Markdown of any page, append .md to the page URL.
> For a complete documentation index, see https://docs.cloudraker.com/llms.txt.
> For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://docs.cloudraker.com/_mcp/server.

# Extraction schema dialect

The `schema` you send to [extract](/capabilities/extract) is JSON Schema, with five constraints. They're checked at the gateway before the run starts, so a bad schema comes back in milliseconds as a `400 invalid_schema` with the offending path and a link to the rule it broke:

```json
{
  "code": "invalid_schema",
  "message": "schema.properties.a.oneOf: `oneOf` is not supported by the extraction schema dialect",
  "retryable": false,
  "requestId": "req_01KYCZWFZW7WX1JM0S0M0VJ6G8",
  "docUrl": "https://docs.cloudraker.com/capabilities/extract/schema#unsupported-keywords"
}
```

The `message` path (`schema.properties.a.oneOf`) points at the exact node to fix.

## Root object

**The root schema must be `{"type": "object"}`.**

Extraction returns one record per document, and a record is a named set of fields. A root array or a bare scalar has no field names to cite against, so grounding would have nothing to attach to. For repeating data, put the array on a property, or use `"unit": "rows_per_document"` to get a row array back from an object schema.

```json title="Rejected"
{
  "type": "array",
  "items": { "type": "object", "properties": { "amount": { "type": "number" } } }
}
```

```json title="Accepted"
{
  "type": "object",
  "properties": {
    "line_items": {
      "type": "array",
      "items": {
        "type": "object",
        "properties": { "amount": { "type": ["number", "null"] } }
      }
    }
  }
}
```

## Max depth

**Nesting may not exceed 5 levels.** The root counts as level 1; every step into `properties`, `items`, `prefixItems`, `patternProperties`, `definitions`, or `additionalProperties` counts as one more.

Deeply nested shapes cost accuracy: the model has to hold the whole path in mind to place a value, and a wrong turn high up invalidates everything under it. Flat, named fields extract better and produce citations you can actually check.

```json title="Rejected — the innermost field sits at level 6"
{
  "type": "object",
  "properties": {
    "a": {
      "type": "object",
      "properties": {
        "b": {
          "type": "object",
          "properties": {
            "c": {
              "type": "object",
              "properties": {
                "d": {
                  "type": "object",
                  "properties": { "e": { "type": "string" } }
                }
              }
            }
          }
        }
      }
    }
  }
}
```

```json title="Accepted — flattened"
{
  "type": "object",
  "properties": {
    "a_b_c_d_e": { "type": ["string", "null"] }
  }
}
```

If you genuinely need the deep shape, extract it flat and reassemble it in your own code.

## Unsupported keywords

**These keywords are rejected:** `$ref`, `$defs`, `oneOf`, `anyOf`, `allOf`, `const`, and `pattern` — anywhere the validator walks, which is every property, array item, and nested object of your schema. Treat them as unsupported everywhere: a keyword that slips past validation is not honoured by the extractor either. Write every field out in full — with `$ref` rejected, a `definitions` block has nothing that can point at it.

The first five make the output shape conditional — the schema no longer says what the result will look like, so neither the extractor nor your code can rely on it. `const` and `pattern` are assertions about a value that was read off a page; failing them tells you nothing useful about the document and would turn a legible field into an error. Constrain values with `enum` and describe formats in `description` or `instructions` instead.

```json title="Rejected"
{
  "type": "object",
  "properties": {
    "party": {
      "oneOf": [
        { "type": "object", "properties": { "person": { "type": "string" } } },
        { "type": "object", "properties": { "company": { "type": "string" } } }
      ]
    },
    "invoice_number": { "type": "string", "pattern": "^INV-[0-9]{6}$" }
  }
}
```

```json title="Accepted"
{
  "type": "object",
  "properties": {
    "party_type": { "type": ["string", "null"], "enum": ["person", "company", null] },
    "party_name": { "type": ["string", "null"] },
    "invoice_number": {
      "type": ["string", "null"],
      "description": "Invoice number, formatted INV-000000."
    }
  }
}
```

A **property named** `pattern` is fine — only keyword positions are checked, and anything under `properties` is a field name, not a keyword.

## Nullable primitives

**Recommended, not enforced:** declare every field that might be absent as nullable — `{"type": ["string", "null"]}` rather than `{"type": "string"}`.

A non-nullable field tells the extractor the value exists. When the document doesn't contain it, that pressure produces a plausible-looking guess instead of an admission. A nullable field gives it somewhere honest to land, and `null` in your result is unambiguous: the field wasn't in the document.

This rule never rejects a request. It's the single highest-impact change you can make to extraction quality.

```json title="Works, but invites invented values"
{
  "type": "object",
  "properties": {
    "po_number": { "type": "string" },
    "due_date": { "type": "string" }
  }
}
```

```json title="Better"
{
  "type": "object",
  "properties": {
    "po_number": { "type": ["string", "null"] },
    "due_date": { "type": ["string", "null"], "description": "ISO 8601 date, or null if absent." }
  }
}
```

## Size limit

**The serialized schema must be 64 KB or smaller.**

The schema travels with the run through internal dispatch, which has a hard message ceiling; the limit is checked up front so you get a clear `400` instead of a failure part-way through a run. In practice 64 KB is hundreds of fields — hitting it usually means one schema is doing several jobs.

```json title="Rejected"
{ "code": "invalid_schema", "message": "schema: serialized schema is 91204 bytes; the limit is 65536" }
```

Split the work instead: run several narrower extractions over the same file (reuse the file id so it's parsed once), or drop fields you don't consume. Long prose belongs in `instructions`, not in per-field `description` repeated across hundreds of properties.

## Next steps

#### [Extract](/capabilities/extract)

The endpoint, its configuration, and the response shape.

#### [Errors](/developers/errors)

The full error envelope and every code the API returns.