Strict Output Validator — C++ source
Check a JSON Schema against OpenAI structured-outputs strict mode rules — open objects, missing required keys, unsupported keywords — before the API rejects it. 100% client-side.
This is the C++ implementation — the same logic the interactive tool runs, in a shareable, citable form.
// Strict Output Validator — validate a JSON Schema against OpenAI's
// structured-outputs strict mode rules, so a schema fails HERE instead of
// at the API.
//
// Language: C++ (C++20, standard library only)
// Port of src/lib/strictOutputValidator.ts (the canonical TypeScript
// implementation). A minimal JSON DOM parser is inlined (objects, arrays,
// strings, numbers, booleans, null) — enough for the schema walker.
// Tool page: https://dev.cosmolabs.org/tools/strict-output-validator
//
// Rules (2026 OpenAI strict mode):
// R1 root must be type "object" (validateStrictRoot)
// R2 every object node needs additionalProperties: false
// R3 every key in properties must be in required
// R4 required must not name keys absent from properties
// R5 only supported type values / keywords may appear
#include <cctype>
#include <cmath>
#include <map>
#include <memory>
#include <set>
#include <sstream>
#include <stdexcept>
#include <string>
#include <variant>
#include <vector>
namespace strict_output_validator {
// ---- minimal JSON DOM ----------------------------------------------------
struct Json;
using JsonPtr = std::shared_ptr<Json>;
using JsonArray = std::vector<JsonPtr>;
using JsonObject = std::vector<std::pair<std::string, JsonPtr>>; // insertion order
struct Json {
enum class Kind { Null, Bool, Num, Str, Arr, Obj } kind = Kind::Null;
bool boolean = false;
double num = 0;
std::string str;
JsonArray items;
JsonObject entries;
bool isObj() const { return kind == Kind::Obj; }
bool isArr() const { return kind == Kind::Arr; }
const Json *get(const std::string &key) const {
if (kind != Kind::Obj) return nullptr;
for (const auto &[k, v] : entries) {
if (k == key) return v.get();
}
return nullptr;
}
};
class ParseError : public std::runtime_error {
public:
explicit ParseError(const std::string &msg) : std::runtime_error(msg) {}
};
namespace detail {
inline void skipWs(const std::string &s, size_t &i) {
while (i < s.size() && std::isspace(static_cast<unsigned char>(s[i]))) i++;
}
inline std::string parseString(const std::string &s, size_t &i) {
if (i >= s.size() || s[i] != '"') throw ParseError("expected string");
i++;
std::string out;
while (i < s.size() && s[i] != '"') {
char c = s[i];
if (c == '\\' && i + 1 < s.size()) {
i++;
char e = s[i];
c = e == 'n' ? '\n' : e == 't' ? '\t' : e == 'r' ? '\r' : e;
}
out += c;
i++;
}
if (i >= s.size()) throw ParseError("unterminated string");
i++; // closing quote
return out;
}
inline JsonPtr parseValue(const std::string &s, size_t &i, int depth) {
if (depth > 256) throw ParseError("nesting too deep");
skipWs(s, i);
if (i >= s.size()) throw ParseError("unexpected end");
auto v = std::make_shared<Json>();
char c = s[i];
if (c == '{') {
v->kind = Json::Kind::Obj;
i++;
skipWs(s, i);
if (i < s.size() && s[i] == '}') { i++; return v; }
while (i < s.size()) {
skipWs(s, i);
std::string key = parseString(s, i);
skipWs(s, i);
if (i < s.size() && s[i] == ':') i++;
v->entries.emplace_back(std::move(key), parseValue(s, i, depth + 1));
skipWs(s, i);
if (i < s.size() && s[i] == ',') { i++; continue; }
if (i < s.size() && s[i] == '}') i++;
break;
}
return v;
}
if (c == '[') {
v->kind = Json::Kind::Arr;
i++;
skipWs(s, i);
if (i < s.size() && s[i] == ']') { i++; return v; }
while (i < s.size()) {
v->items.push_back(parseValue(s, i, depth + 1));
skipWs(s, i);
if (i < s.size() && s[i] == ',') { i++; continue; }
if (i < s.size() && s[i] == ']') i++;
break;
}
return v;
}
if (c == '"') {
v->kind = Json::Kind::Str;
v->str = parseString(s, i);
return v;
}
if (s.compare(i, 4, "true") == 0) {
i += 4;
v->kind = Json::Kind::Bool;
v->boolean = true;
return v;
}
if (s.compare(i, 5, "false") == 0) {
i += 5;
v->kind = Json::Kind::Bool;
return v;
}
if (s.compare(i, 4, "null") == 0) {
i += 4;
return v; // Null
}
if (c == '-' || (c >= '0' && c <= '9')) {
size_t end = 0;
v->num = std::stod(s.substr(i), &end);
i += end;
v->kind = Json::Kind::Num;
return v;
}
throw ParseError(std::string("unexpected character: ") + c);
}
} // namespace detail
inline JsonPtr parseJson(const std::string &src) {
size_t i = 0;
JsonPtr v = detail::parseValue(src, i, 0);
detail::skipWs(src, i);
if (i != src.size()) throw ParseError("trailing content");
return v;
}
// ---- validator ------------------------------------------------------------
enum class StrictRule {
RootNotObject,
MissingAdditionalProperties,
PropertyNotRequired,
RequiredNotProperty,
UnsupportedType,
UnsupportedKeyword,
InvalidSchema,
};
inline const char *ruleName(StrictRule r) {
switch (r) {
case StrictRule::RootNotObject: return "root-not-object";
case StrictRule::MissingAdditionalProperties: return "missing-additional-properties";
case StrictRule::PropertyNotRequired: return "property-not-required";
case StrictRule::RequiredNotProperty: return "required-not-property";
case StrictRule::UnsupportedType: return "unsupported-type";
case StrictRule::UnsupportedKeyword: return "unsupported-keyword";
default: return "invalid-schema";
}
}
struct StrictIssue {
std::string path;
StrictRule rule;
std::string message;
};
struct StrictReport {
bool ok = true;
std::vector<StrictIssue> issues;
struct {
long objects = 0;
long properties = 0;
long enums = 0;
} counts;
};
inline bool typeSupported(const std::string &t) {
static const std::set<std::string> supported = {
"object", "array", "string", "number", "integer", "boolean"};
return supported.count(t) > 0;
}
inline bool keywordSupported(const std::string &k) {
static const std::set<std::string> supported = {
"type", "description", "title", "properties", "required",
"additionalProperties", "items", "enum", "const", "anyOf", "allOf",
"$ref", "$defs", "definitions", "format", "nullable", "default"};
return supported.count(k) > 0;
}
struct Walker {
StrictReport rep;
void push(const std::string &path, StrictRule rule, const std::string &message) {
rep.issues.push_back({path, rule, message});
}
void walk(const Json &node, const std::string &path) {
// R5 keywords
for (const auto &[key, dummy] : node.entries) {
if (!keywordSupported(key)) {
push(path, StrictRule::UnsupportedKeyword,
"\"" + key + "\" is not supported in strict mode — remove it or "
"express the constraint another way.");
}
}
// R5 types ('null' only inside a type array).
const Json *type = node.get("type");
if (type != nullptr) {
if (type->kind == Json::Kind::Str) {
if (!typeSupported(type->str)) {
push(path, StrictRule::UnsupportedType,
"type \"" + type->str + "\" is not supported — strict mode allows "
"object, array, string, number, integer, boolean (null only "
"inside a type array).");
}
} else if (type->kind == Json::Kind::Arr) {
for (const auto &t : type->items) {
bool ok = t->kind == Json::Kind::Str
&& (typeSupported(t->str) || t->str == "null");
if (!ok) {
push(path, StrictRule::UnsupportedType,
"a type-array entry is not supported — strict mode allows "
"object, array, string, number, integer, boolean, null.");
}
}
}
}
// allOf is accepted only as a single-element wrapper.
const Json *all_of = node.get("allOf");
if (all_of != nullptr && all_of->isArr() && all_of->items.size() != 1) {
push(path, StrictRule::UnsupportedKeyword,
"allOf is supported only with exactly one subschema (use anyOf for unions).");
}
// Object node: R2, R3, R4.
bool is_object_node =
(type != nullptr && type->kind == Json::Kind::Str && type->str == "object")
|| node.get("properties") != nullptr
|| node.get("required") != nullptr;
if (is_object_node) {
rep.counts.objects++;
const Json *ap = node.get("additionalProperties");
bool ap_false = ap != nullptr && ap->kind == Json::Kind::Bool && !ap->boolean;
if (!ap_false) {
push(path, StrictRule::MissingAdditionalProperties,
"Object needs \"additionalProperties\": false — strict mode rejects "
"open objects.");
}
const Json *props = node.get("properties");
const Json *required = node.get("required");
std::set<std::string> req_keys;
if (required != nullptr && required->isArr()) {
for (const auto &r : required->items) {
if (r->kind == Json::Kind::Str) req_keys.insert(r->str);
}
}
if (props != nullptr && props->isObj()) {
rep.counts.properties += static_cast<long>(props->entries.size());
for (const auto &[key, sub] : props->entries) {
if (req_keys.count(key) == 0) {
push(path + ".required", StrictRule::PropertyNotRequired,
"\"" + key + "\" is defined in properties but missing from "
"required — strict mode requires every property.");
}
}
for (const auto &k : req_keys) {
if (props->get(k) == nullptr) {
push(path + ".required", StrictRule::RequiredNotProperty,
"\"" + k + "\" is required but has no definition in properties.");
}
}
for (const auto &[key, sub] : props->entries) {
if (sub->isObj()) walk(*sub, path + ".properties." + key);
}
} else if (!req_keys.empty()) {
for (const auto &k : req_keys) {
push(path + ".required", StrictRule::RequiredNotProperty,
"\"" + k + "\" is required but has no definition in properties.");
}
}
}
const Json *items = node.get("items");
if (items != nullptr && items->isObj()) walk(*items, path + ".items");
const Json *enumv = node.get("enum");
if (enumv != nullptr && enumv->isArr()) rep.counts.enums++;
static const char *lists[] = {"anyOf", "oneOf", "allOf"};
for (int li = 0; li < 3; li++) {
const Json *list = node.get(lists[li]);
if (list != nullptr && list->isArr()) {
if (li == 1) { // oneOf
push(path + "." + lists[li], StrictRule::UnsupportedKeyword,
"oneOf is not supported — strict mode unions are expressed with "
"anyOf.");
}
for (size_t i = 0; i < list->items.size(); i++) {
if (list->items[i]->isObj()) {
walk(*list->items[i],
path + "." + lists[li] + "[" + std::to_string(i) + "]");
}
}
}
}
static const char *defs_keys[] = {"$defs", "definitions"};
for (const char *defs_key : defs_keys) {
const Json *defs = node.get(defs_key);
if (defs != nullptr && defs->isObj()) {
for (const auto &[name, sub] : defs->entries) {
if (sub->isObj()) {
walk(*sub, path + "." + defs_key + "." + name);
}
}
}
}
}
};
/** validateStrictSchema over a parsed DOM. */
inline StrictReport validateStrictSchema(const Json &schema) {
Walker w;
if (!schema.isObj()) {
w.push("$", StrictRule::InvalidSchema, "Schema must be a JSON object.");
w.rep.ok = false;
return w.rep;
}
w.walk(schema, "$");
w.rep.ok = w.rep.issues.empty();
return w.rep;
}
/** The whole-report entry point: parse + validate + R1 (root must be
* type "object"). `input` is a JSON string. */
inline StrictReport validateStrictRoot(const std::string &input) {
JsonPtr schema;
try {
schema = parseJson(input);
} catch (const std::exception &e) {
StrictReport rep;
rep.ok = false;
rep.issues.push_back({"$", StrictRule::InvalidSchema,
std::string("Not valid JSON: ") + e.what()});
return rep;
}
StrictReport rep = validateStrictSchema(*schema);
const Json *type = schema->get("type");
if (!(type != nullptr && type->kind == Json::Kind::Str && type->str == "object")) {
rep.issues.insert(rep.issues.begin(),
{"$", StrictRule::RootNotObject,
"The root schema must be type \"object\" — strict mode cannot "
"return a bare scalar or array."});
rep.ok = false;
}
return rep;
}
} // namespace strict_output_validator
Also available in 13 other languages
Every CosmoDev tool ships its pure logic in TypeScript (web) and Go (CLI), with authored implementations in a dozen-plus languages — the same contract, ported. Compare all languages side by side →