DocsSchemas & formats

Schemas & formats

The full schema contract: which keywords steer the model, which are enforced by the runtime, and every format that computes, decodes, validates or normalizes a value for you.

Your JSON Schema is the contract. It defines the shape of the output, it steers what the model looks for, and through format it decides which values Veralens computes, decodes, validates or normalizes for you after extraction.

This page is the full list of what the schema controls.

Which keywords do what

Not every keyword reaches the model, and that is deliberate. A model is not a validator: given multipleOf, it will confidently return a value that violates it. So constraints are applied by the runtime, after extraction, where they are enforceable.

KeywordHandled byEffect
type, properties, items, requiredModelDefines the output structure.
descriptionModelThe primary steering lever. State what the field means, its units, and the format you want, in plain language.
enumModelClosed set. The model must pick one of the listed values.
defaultRuntimeUsed when a value is absent.
formatRuntimeSelects an addon. Full table below.
multipleOf, minimum, maximum, exclusiveMinimum, exclusiveMaximumRuntimeNumeric values are clamped to the range and snapped to the step after extraction. multipleOf: 0.01 is how you pin money to two decimals.
type: "integer"RuntimeResult is rounded to a whole number.
pattern, minLength, maxLengthNeitherNot enforced. Put the rule in description instead.

Numbers

Two things happen to a numeric field that you do not have to write yourself.

Calculations run in code, not in the model. When a value has to be derived (a tax base from a total and a rate, a line amount from quantity and unit price), the model identifies the quantities and how they combine, and the runtime performs the arithmetic. Deterministic, correct precedence, rounded once at the end.

Constraints are applied to the result. Declare the decimal step you want and you get it:

json
{
  "type": "object",
  "properties": {
    "subtotal": { "type": "number", "multipleOf": 0.01 },
    "tax":      { "type": "number", "multipleOf": 0.01 },
    "total":    { "type": "number", "multipleOf": 0.01 },
    "quantity": { "type": "integer" }
  }
}

You get 110.6, never 110.60000000000001.

The format table

Add format to a field and the runtime resolves the value after extraction. The model always reads what is printed; the conversion happens afterwards.

formatField typeYou get
date (or isodate)stringISO 8601 date, 2026-03-14
date-time (or isodatetime)stringISO 8601 date and time
countrystringISO 3166-1 alpha-2, PT
currencystringISO 4217, EUR
languagestringISO 639-1, pt
phone (or phone-number)stringE.164, +351912345678
ibanstringCompact IBAN, verified with the ISO 13616 mod-97 checksum
nif (or taxid)stringTax identifier, checksum verified
emailstringCanonical lowercase address
uri (or url)stringCleaned absolute URL
qrcode (or qr)stringThe decoded payload of a QR code on the page
barcodestringThe decoded payload of a linear barcode
bbox (or boundingbox, segment)array[ymin, xmin, ymax, xmax] in pixels of the source image, or in a 0 to 1000 space when the dimensions are unknown

The rule that governs all of them

Resolve or leave alone. Never fabricate.

A value that does not resolve comes back exactly as printed, unchanged. An IBAN that fails its checksum is returned as raw text and is not silently corrected. A currency word with no ISO mapping stays the word. A date whose parts are genuinely ambiguous is not forced into a confident wrong ISO string.

This is deliberate: a value that came through raw is visible to you and can be routed for review, while a quietly guessed one is undetectable.

Codes are decoded, not read

qrcode and barcode fields are not the model reading the symbol off the pixels. Every image and page is passed through a barcode decoder before extraction, and the decoded payload is what fills the field. QR, Micro QR and the common linear families (EAN, UPC, Code 128, Code 39, ITF) are supported.

**If the page carries no readable code, the field is null.** It is never filled with something that looks like a payload.

json
{
  "type": "object",
  "properties": {
    "fiscal_qr":   { "type": "string", "format": "qrcode" },
    "product_ean": { "type": "string", "format": "barcode" }
  }
}

One schema for mixed documents

You do not have to know which document you are sending. Describe every shape you accept as branches of an anyOf, give each branch a discriminating enum field, and each entry comes back as the shape it actually is:

json
{
  "type": "object",
  "properties": {
    "documents": {
      "type": "array",
      "items": {
        "anyOf": [
          {
            "type": "object",
            "properties": {
              "kind":     { "type": "string", "enum": ["invoice"] },
              "supplier": { "type": "string" },
              "total":    { "type": "number", "multipleOf": 0.01 }
            },
            "required": ["kind", "total"]
          },
          {
            "type": "object",
            "properties": {
              "kind":            { "type": "string", "enum": ["id_card"] },
              "document_number": { "type": "string" },
              "nationality":     { "type": "string", "format": "country" },
              "expires_at":      { "type": "string", "format": "date" }
            },
            "required": ["kind", "document_number"]
          }
        ]
      }
    }
  }
}

Classification and extraction happen in the same pass, so this replaces a separate classify-then-route step.

Composition keywords: anyOf and oneOf are passed through, $ref and $defs (or definitions) are resolved, and allOf branches are merged into one shape. discriminator is ignored, so use an enum field as your discriminator, which also narrows a TypeScript union directly.

Absent fields

A field that is genuinely not on the document comes back null. Extraction is built to abstain rather than to produce a plausible value, because a wrong value you cannot detect costs more than a gap you can.

Two consequences for your schema:

  • **Keep required short.** Everything you mark required is pressure to produce a value. Reserve it for fields that are genuinely always present.
  • **Handle null in your application** rather than forcing the extraction to guess. Route it to review.

Markdown and plaintext targets

With target: "markdown" or "plaintext" and no schema, you get the whole document as one string. Elements that are not text still get represented rather than dropped:

On the pagemarkdownplaintext
Signature<signature>A. Silva</signature>the transcription, or [signature]
Watermark<watermark>PAID</watermark>the text, or [watermark]
Stamp<stamp>…</stamp>the text, or [stamp]
Embedded image<image type="image/png">description</image>the description
Redacted span<redacted/>[redacted]
QR code<qrcode>decoded payload</qrcode>the decoded payload
Barcode<barcode>decoded payload</barcode>the decoded payload

Markdown output uses HTML <table> for tabular data, LaTeX for mathematics, and Mermaid for diagrams that can be recovered.

Multiple sources in one job

sources takes an array, and every source is read as part of the same document set. Send the front and back of an ID card, or a set of receipts, and the schema is filled from all of them together.

Next