Get schema-valid JSON out of any LLM from C++: validate the reply, and retry with the errors until it conforms.
Part of llm-cpp, a family of 26 single-header C++ libraries for building on LLM APIs. Each one stands alone: copy one header, include it, done.
Asking a model for JSON works most of the time, and the rest of the time you get markdown fences, a missing field or a string where a number should be. llm-format turns your schema into prompt instructions, strips fences, validates the result, and re-asks the model with the specific validation errors. It has no HTTP code: you pass in a function that calls whichever model you use.
- Declare a schema as fields with name, type (
string,number,bool,array,object), required flag and description schema_to_prompt()generates the formatting instructions appended to your promptenforce_schema(): call your LLM, strip markdown fences, parse, validate, and retry with a correction prompt (default 3 attempts)validate()on its own: missing required fields, wrong types, and optional rejection of unexpected fields- Built-in JSON parser and serializer (
JsonValue,parse_json,to_json) - Provider-agnostic: works with any
std::string(const std::string&)callable
Requirements: a C++17 compiler. No other dependencies, no network access.
- Copy
include/llm_format.hppinto your project. - In exactly one
.cppfile,#define LLM_FORMAT_IMPLEMENTATIONbefore including it. Other files just#include "llm_format.hpp".
#define LLM_FORMAT_IMPLEMENTATION
#include "llm_format.hpp"
#include <iostream>
// Plug in any LLM client: a function that takes a prompt and returns text.
std::string my_llm(const std::string& prompt) {
(void)prompt;
return "```json\n{\"name\": \"Ada Lovelace\", \"born\": 1815, \"programmer\": true}\n```";
}
int main() {
llm::Schema schema;
schema.name = "Person";
schema.fields = {
{"name", "string", true, "Full name"},
{"born", "number", true, "Year of birth"},
{"programmer", "bool", true, "Wrote programs"},
};
llm::FormatResult r = llm::enforce_schema("Tell me about Ada Lovelace.", schema, my_llm);
if (r.valid)
std::cout << r.value["name"].as_string() << " (" << r.value["born"].as_number() << ")"
<< ", attempts: " << r.attempts_used << "\n";
}Build and run:
g++ -std=c++17 -I include example.cpp -o example
./exampleOutput:
Ada Lovelace (1815), attempts: 1
Everything lives in namespace llm.
| Function / type | What it does |
|---|---|
enforce_schema(prompt, schema, llm_fn, cfg) |
Return a FormatResult (parsed value, attempts used, valid flag, raw response) |
validate(value, schema, allow_extra) |
Return a ValidationResult with a list of error strings |
schema_to_prompt(schema) |
Render the schema as instructions for the model |
parse_json(str), to_json(value, pretty) |
JSON parsing and serialization |
FormatConfig |
max_retries (default 3), strip_markdown, allow_extra_fields |
The first attempt sends your prompt plus the rendered schema. Each reply is stripped of markdown code fences, parsed, and validated against the schema. If it fails, the next prompt tells the model exactly what was wrong ("Missing required field", "wrong type") and asks again, up to max_retries.
The examples/ folder has runnable programs:
Build the examples with CMake:
cmake -B build
cmake --build build- The schema describes the top-level object's fields and their types; nested objects and arrays are type-checked but their contents are not validated against a sub-schema.
- This is its own schema format, not JSON Schema.
MIT. See LICENSE.