# LLM Output Validator: JSON Schema + Repair Hints (`codeclouds/llm-output-validator`) Actor

Validate LLM/tool-call JSON output against a JSON Schema (draft-07 or 2020-12). Returns machine-readable error paths plus concrete, actionable repair hints per error, and optional deterministic normalization/repair — so an agent can self-correct without an extra LLM round-trip.

- **URL**: https://apify.com/codeclouds/llm-output-validator.md
- **Developed by:** [Dennis](https://apify.com/codeclouds) (community)
- **Categories:**
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $1.50 / 1,000 document-validateds

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.

Learn more: https://docs.apify.com/actors/running/actors-in-store.md#pay-per-event

## What's an Apify Actor?

Actors are web data automations that power AI and operations. They run on the Apify platform to scrape websites, process data, connect APIs, and automate workflows.
In Batch mode, an Actor accepts a well-defined JSON input, performs an action which can take anything from a few seconds to a few hours,
and optionally produces a well-defined JSON output, datasets with results, or files in key-value store.
In Standby mode, an Actor provides a web server which can be used as a website, API, or an MCP server.
Actors are written with capital "A".

## How to integrate an Actor?

If asked about integration, you help developers integrate Actors into their projects.
You adapt to their stack and deliver integrations that are safe, well-documented, and production-ready.
The best way to integrate Actors is as follows.

- **AI agents and MCP clients** — the [Apify MCP server](https://docs.apify.com/integrations/mcp.md) at `https://mcp.apify.com` (remote, streamable HTTP, OAuth on first use).
- **Agentic workflows and local Actor development** — [Agent Skills](https://apify.com/.well-known/agent-skills/index.json) with the [Apify CLI](https://docs.apify.com/cli/docs.md): `npm install -g apify-cli`, then `apify login`.
- **JavaScript/TypeScript projects** — the official [JS/TS client](https://docs.apify.com/api/client/js/docs.md): `npm install apify-client`.
- **Python projects** — the official [Python client](https://docs.apify.com/api/client/python/docs.md): `pip install apify-client`.
- **Any other language** — the [REST API](https://docs.apify.com/api/v2.md).

For usage examples, see the [API](#api) section below.

For more details, see Apify documentation as [Markdown index](https://docs.apify.com/llms.txt) and [Markdown full-text](https://docs.apify.com/llms-full.txt).

# README

## LLM Output Validator: JSON Schema + Repair Hints

Validate LLM or tool-call JSON output against a JSON Schema (draft-07 or 2020-12) and get back **machine-readable error paths plus a concrete, actionable repair hint for every single error** — not just AJV's raw "must be integer" message, but something like *"At /age: change the value to type 'integer' (currently string, value '30'). If this looks like a stringified number/boolean, running with 'normalize': true may fix it automatically."* Optionally also get deterministic, information-preserving normalization (`"42"` → `42`, whitespace trimmed) with every applied change logged, and a fully **repaired document** when every error in it was fixable this way. 100% deterministic, pure computation — no network calls, no LLM calls, no scraping.

### When should an AI agent use this?

- "My agent just called a tool and I need to check the JSON response matches the tool's expected schema before passing it downstream."
- "Validate this batch of 50 LLM-generated JSON records against our internal data schema and tell me exactly which ones are broken and why."
- "I got `{\"age\": \"30\"}` back from a model but my schema wants `age` as an integer — can this be fixed automatically without another LLM call?"
- "Give me a structured list of schema violations (with instancePath and schemaPath) I can feed straight back into a retry prompt."
- "Check whether this OpenAI/Anthropic tool-call argument JSON actually satisfies my function's parameter schema."
- "I want an audit trail of every value my pipeline silently coerced, not a black box."

### What this Actor does

- Validates each document in `documents` against the JSON Schema in `schema`, using [AJV](https://ajv.js.org/) — supports both **draft-07** and **2020-12** (auto-detected from the schema's own `$schema` field; defaults to draft-07 when absent).
- Returns, per document: `valid` (boolean), `errors[]` with `instancePath`, `schemaPath`, `keyword`, `message`, and (when `hints` is on) a `hint` string that names the exact fix — not a restatement of the error.
- Optional deterministic **normalization**: safe, information-preserving coercions only (string→number/integer/boolean when the schema requires it and the value round-trips exactly, number/boolean→string, whitespace trimming) — logged per change in `appliedTransforms[]`. Never guesses: an ambiguous value (e.g. `"007"`, `"yes"`) is left untouched and its error stays in `errors[]`.
- `repairable` + `repairedData`: a document is only reported as `repairable: true` (with a `repairedData` payload) when **every single error** in it was resolved by the deterministic normalization layer above — otherwise `repairable: false` and no `repairedData`, so you never get a partially-fixed document silently presented as "done".
- One flat, MCP-friendly JSON record per document in the dataset — easy for an AI agent to consume directly as a tool result.

### Input

| Field | Type | Description |
|---|---|---|
| `documents` | array of strings | One or more JSON documents to validate, each given as a **JSON string** (e.g. `"{\"name\":\"Alice\",\"age\":30}"`). A string that fails to parse as JSON is reported as a per-document error, not a run failure. Example: `["{\"name\": \"Alice\", \"age\": 30}"]`. |
| `schema` | object | The JSON Schema (draft-07 or 2020-12) every document must satisfy. Add `"$schema": "https://json-schema.org/draft/2020-12/schema"` to use 2020-12-only features; otherwise draft-07 is assumed. |
| `hints` | boolean | Generate a concrete repair hint per error. Default `true`. |
| `normalize` | boolean | Apply safe, deterministic type coercions and return `normalizedData`/`appliedTransforms`/`repairedData`. Default `false`. |
| `maxErrorsPerDocument` | integer | Caps how many error entries are *returned* per document (`errorCount`/`truncated` always reflect the real, uncapped total). Default `20`, max `1000`. |

### Output

One flat JSON record per validated document:

```json
{
  "index": 1,
  "valid": false,
  "errors": [
    {
      "instancePath": "/age",
      "schemaPath": "#/properties/age/type",
      "keyword": "type",
      "message": "must be integer",
      "hint": "At /age: change the value to type \"integer\" (currently string, value \"25\"). If this looks like a stringified number/boolean, running with \"normalize\": true may fix it automatically."
    }
  ],
  "errorCount": 1,
  "truncated": false,
  "repairable": true,
  "normalizedData": { "name": "Bob", "age": 25, "email": "bob@example.com" },
  "appliedTransforms": [
    { "path": "/age", "from": "25", "to": 25, "reason": "coerced string to integer (schema requires numeric type at this path)" }
  ],
  "repairedData": { "name": "Bob", "age": 25, "email": "bob@example.com" }
}
```

A document that fails to parse as JSON in the first place:

```json
{
  "index": 3,
  "valid": false,
  "errors": [
    {
      "instancePath": "",
      "schemaPath": "",
      "keyword": "json-parse",
      "message": "Unexpected token 'o', \"not valid json\" is not valid JSON",
      "hint": "This document string is not valid JSON — fix the JSON syntax (unescaped quotes, trailing commas, unquoted keys) before revalidating. No schema check could run."
    }
  ],
  "errorCount": 1,
  "truncated": false,
  "repairable": false,
  "parseError": "Unexpected token 'o', \"not valid json\" is not valid JSON"
}
```

| Field | Description |
|---|---|
| `index` | 0-based position of this document in the `documents` input array |
| `valid` | Whether the document satisfies `schema` |
| `errors` | Up to `maxErrorsPerDocument` errors, each with `instancePath`/`schemaPath`/`keyword`/`message` and (if `hints: true`) `hint` |
| `errorCount` | The REAL total error count, even if `errors` was capped |
| `truncated` | `true` if `errorCount` is larger than the returned `errors` array |
| `repairable` | `true` if the document is already valid, or if EVERY error in it was resolved by deterministic normalization |
| `normalizedData` | Only present when `normalize: true` — the document with safe coercions applied |
| `appliedTransforms` | Only present when `normalize: true` — every coercion that was applied, with `path`/`from`/`to`/`reason` |
| `repairedData` | Only present when `normalize: true` AND `repairable: true` AND the document was originally invalid |
| `parseError` | Only present if the input string itself was not valid JSON |

### Use cases

- Guard a function-calling / MCP pipeline: validate a model's tool-call arguments before executing the tool, and feed the `hint` strings straight back into a retry prompt instead of a raw AJV error dump.
- Batch-audit a dataset of LLM-generated JSON records against a target schema before ingesting them into a downstream system.
- Give an agent framework a single deterministic "validate + try to self-heal" step that never silently guesses at ambiguous data.
- Regression-test your own JSON Schema definitions against known-good/known-bad example payloads.
- Build an audit trail of exactly which values a pipeline coerced and why, instead of a black-box "it worked" or "it didn't".

### Pricing

This Actor uses Apify's Pay-Per-Event (PPE) pricing model.

- **Actor Start:** $0.00005 (Apify default)
- **`document-validated`:** $0.0015 per document validated (charged once per document in `documents`, regardless of whether it was valid, invalid, or unparsable JSON)

See [STOREINFO.md](STOREINFO.md) for the full pricing table and rationale.

### Legal

This Actor performs pure computation on documents and a JSON Schema you supply directly as input — it does not scrape, crawl, or fetch any external data, and does not collect or store personal data beyond what you choose to submit as `documents` for the duration of the run's dataset. You remain fully responsible for the content of the documents you submit and for how you use the validation/normalization results downstream. Normalization is intentionally conservative (see "What this Actor does" above) — it never invents or reinterprets ambiguous values, so it will never silently change the meaning of your data.

### FAQ

**Does this call an LLM to figure out how to fix errors?**
No. Every hint and every normalization decision is deterministic, rule-based logic derived directly from AJV's error output and the schema's own declared types — no model call, no randomness, no network access at runtime.

**What's the difference between `normalizedData` and `repairedData`?**
`normalizedData` is always the best-effort result of applying safe coercions, whether or not that fully fixes the document. `repairedData` is only present when normalization fixed **every** error — i.e. the document is now fully schema-valid. If even one error remains, you get `repairable: false` and no `repairedData`, so you never mistake a partial fix for a complete one.

**Will `normalize` ever change a value in a way that could be wrong?**
No — by design. It only coerces a string to a number/boolean when the string round-trips exactly (e.g. `"42"` → `42`, but NOT `"007"` or `"1e3"`, which are rejected as ambiguous), only accepts the literal strings `"true"`/`"false"` for booleans (never `"yes"`/`"1"`), and only trims whitespace. Anything it's not fully sure about is left untouched.

**Does this support `$ref` in my schema during normalization?**
Validation itself fully supports `$ref` (AJV resolves it natively). The normalization/repair layer does not resolve `$ref` in v1 — a subschema reached only through a `$ref` is passed through unchanged rather than guessed at. Validation errors in that subtree still appear in `errors[]` as normal; they're just not eligible for automatic repair yet.

**What happens if my `schema` itself is invalid?**
Every document in that run is reported with a `schema-invalid` error explaining why, and none are charged — fix the schema and run again.

**Why is `documents` an array of strings instead of an array of objects?**
Apify's input-schema format doesn't support array items that can be either an object or a string, so every document is provided as a JSON string (works equally well whether your data started as an object or as raw model output text) — see the Input table above for an example.

### Keywords

json schema validator, llm output validation, function calling validator, tool call validation, mcp tool output check, structured output validator, json repair, schema conformance checker, ajv validator actor, agent output validation, self-healing json, deterministic json repair, draft-07 validator, 2020-12 json schema, llm guardrails, structured outputs verification

### Changelog

#### 0.1.0

- Initial release: draft-07 and 2020-12 JSON Schema validation via AJV, keyword-specific repair hints for every AJV error type, deterministic normalization layer with full transform logging, `repairable`/`repairedData` semantics.

# Actor input Schema

## `documents` (type: `array`):

One or more JSON documents to validate, each as a JSON string (e.g. the raw text an LLM/tool call produced). Example: \["{"name":"Alice","age":30}"]. A string that fails to parse as JSON is reported as a per-document error, not a run failure.

## `schema` (type: `object`):

The JSON Schema (draft-07 or 2020-12) every document must satisfy. Add "$schema": "https://json-schema.org/draft/2020-12/schema" to use 2020-12 features (e.g. prefixItems) — otherwise draft-07 is assumed.

## `hints` (type: `boolean`):

When enabled, every error also gets a concrete, actionable "hint" string (not just AJV's raw message) describing exactly how to fix it. Disable only if you already have your own hint logic downstream and want to save a little compute.

## `normalize` (type: `boolean`):

When enabled, returns normalizedData: a copy of the document with deterministic, information-preserving type coercions applied (e.g. "42" -> 42 when the schema requires an integer, whitespace trimmed from strings) plus appliedTransforms logging exactly what changed. Never guesses — an ambiguous value is left untouched and its error stays in errors\[]. Off by default so output shape stays minimal unless you opt in.

## `maxErrorsPerDocument` (type: `integer`):

Caps how many error entries are returned per document (errorCount and truncated always reflect the real, uncapped total). Useful to keep a single malformed document from flooding the dataset with hundreds of nested errors.

## Actor input object example

```json
{
  "documents": [
    "{\"name\": \"Alice\", \"age\": 30, \"email\": \"alice@example.com\"}"
  ],
  "schema": {
    "type": "object",
    "properties": {
      "name": {
        "type": "string"
      },
      "age": {
        "type": "integer",
        "minimum": 0
      },
      "email": {
        "type": "string",
        "format": "email"
      }
    },
    "required": [
      "name",
      "age",
      "email"
    ],
    "additionalProperties": false
  },
  "hints": true,
  "normalize": false,
  "maxErrorsPerDocument": 20
}
```

# Actor output Schema

## `results` (type: `string`):

Results stored in the default dataset.

# API

You can run this Actor programmatically using our API. Below are code examples in JavaScript, Python, and CLI, as well as the OpenAPI specification and MCP server setup.

## JavaScript example

```javascript
import { ApifyClient } from 'apify-client';

// Initialize the ApifyClient with your Apify API token
// Replace the '<YOUR_API_TOKEN>' with your token
const client = new ApifyClient({
    token: '<YOUR_API_TOKEN>',
});

// Prepare Actor input
const input = {
    "documents": [
        "{\"name\": \"Alice\", \"age\": 30, \"email\": \"alice@example.com\"}"
    ],
    "schema": {
        "type": "object",
        "properties": {
            "name": {
                "type": "string"
            },
            "age": {
                "type": "integer",
                "minimum": 0
            },
            "email": {
                "type": "string",
                "format": "email"
            }
        },
        "required": [
            "name",
            "age",
            "email"
        ],
        "additionalProperties": false
    }
};

// Run the Actor and wait for it to finish
const run = await client.actor("codeclouds/llm-output-validator").call(input);

// Fetch and print Actor results from the run's dataset (if any)
console.log('Results from dataset');
console.log(`💾 Check your data here: https://console.apify.com/storage/datasets/${run.defaultDatasetId}`);
const { items } = await client.dataset(run.defaultDatasetId).listItems();
items.forEach((item) => {
    console.dir(item);
});

// 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/js/docs

```

## Python example

```python
from apify_client import ApifyClient

# Initialize the ApifyClient with your Apify API token
# Replace '<YOUR_API_TOKEN>' with your token.
client = ApifyClient("<YOUR_API_TOKEN>")

# Prepare the Actor input
run_input = {
    "documents": ["{\"name\": \"Alice\", \"age\": 30, \"email\": \"alice@example.com\"}"],
    "schema": {
        "type": "object",
        "properties": {
            "name": { "type": "string" },
            "age": {
                "type": "integer",
                "minimum": 0,
            },
            "email": {
                "type": "string",
                "format": "email",
            },
        },
        "required": [
            "name",
            "age",
            "email",
        ],
        "additionalProperties": False,
    },
}

# Run the Actor and wait for it to finish
run = client.actor("codeclouds/llm-output-validator").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print(f"💾 Check your data here: https://console.apify.com/storage/datasets/{run.default_dataset_id}")
for item in client.dataset(run.default_dataset_id).iterate_items():
    print(item)

# 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/python/docs/quick-start

```

## CLI example

```bash
echo '{
  "documents": [
    "{\\"name\\": \\"Alice\\", \\"age\\": 30, \\"email\\": \\"alice@example.com\\"}"
  ],
  "schema": {
    "type": "object",
    "properties": {
      "name": {
        "type": "string"
      },
      "age": {
        "type": "integer",
        "minimum": 0
      },
      "email": {
        "type": "string",
        "format": "email"
      }
    },
    "required": [
      "name",
      "age",
      "email"
    ],
    "additionalProperties": false
  }
}' |
apify call codeclouds/llm-output-validator --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,codeclouds/llm-output-validator"
        }
    }
}

```

The hosted server signs you in with OAuth on first connect, so no API token belongs in this config. Clients without OAuth support can send an `Authorization: Bearer <APIFY_API_TOKEN>` header instead, using a token from API & Integrations in Apify Console (https://console.apify.com/settings/integrations).

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/actors/SisaN3xtZI1Quswis/builds/xHdptfzJy3i5tymFA/openapi.json
