Skip to content

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 →