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 (C11, 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 <stdarg.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

/* ---- minimal JSON DOM ------------------------------------------------- */

typedef enum { JSON_NULL, JSON_BOOL, JSON_NUM, JSON_STR, JSON_ARR, JSON_OBJ } json_kind;

typedef struct json json;
struct json {
    json_kind kind;
    int boolean;
    double num;
    char *str;            /* JSON_STR */
    json **items;         /* JSON_ARR */
    size_t item_count;
    char **keys;          /* JSON_OBJ (keys[i] -> values[i]) */
    json **values;
    size_t entry_count;
};

typedef struct {
    const char *src;
    size_t pos;
    int depth;
} jparse;

static json *jnew(json_kind k) {
    json *v = calloc(1, sizeof(json));
    if (v) v->kind = k;
    return v;
}

static void jskip(jparse *p) {
    while (p->src[p->pos] == ' ' || p->src[p->pos] == '\t'
        || p->src[p->pos] == '\n' || p->src[p->pos] == '\r') {
        p->pos++;
    }
}

static json *jvalue(jparse *p);

static char *jstring(jparse *p) {
    if (p->src[p->pos] != '"') return NULL;
    p->pos++;
    size_t cap = 32, len = 0;
    char *out = malloc(cap);
    while (p->src[p->pos] && p->src[p->pos] != '"') {
        char c = p->src[p->pos];
        if (c == '\\' && p->src[p->pos + 1]) {
            p->pos++;
            char e = p->src[p->pos];
            c = e == 'n' ? '\n' : e == 't' ? '\t' : e == 'r' ? '\r' : e;
        }
        if (len + 2 > cap) {
            cap *= 2;
            out = realloc(out, cap);
        }
        out[len++] = c;
        p->pos++;
    }
    if (p->src[p->pos] == '"') p->pos++;
    out[len] = '\0';
    return out;
}

static json *jvalue(jparse *p) {
    jskip(p);
    if (p->depth > 256) return NULL;
    char c = p->src[p->pos];
    if (c == '{') {
        p->pos++;
        p->depth++;
        json *o = jnew(JSON_OBJ);
        jskip(p);
        if (p->src[p->pos] == '}') {
            p->pos++;
            p->depth--;
            return o;
        }
        while (1) {
            jskip(p);
            char *key = jstring(p);
            jskip(p);
            if (p->src[p->pos] == ':') p->pos++;
            json *val = jvalue(p);
            if (!key || !val) break;
            o->keys = realloc(o->keys, (o->entry_count + 1) * sizeof(char *));
            o->values = realloc(o->values, (o->entry_count + 1) * sizeof(json *));
            o->keys[o->entry_count] = key;
            o->values[o->entry_count] = val;
            o->entry_count++;
            jskip(p);
            if (p->src[p->pos] == ',') { p->pos++; continue; }
            if (p->src[p->pos] == '}') p->pos++;
            break;
        }
        p->depth--;
        return o;
    }
    if (c == '[') {
        p->pos++;
        p->depth++;
        json *a = jnew(JSON_ARR);
        jskip(p);
        if (p->src[p->pos] == ']') {
            p->pos++;
            p->depth--;
            return a;
        }
        while (1) {
            json *val = jvalue(p);
            if (!val) break;
            a->items = realloc(a->items, (a->item_count + 1) * sizeof(json *));
            a->items[a->item_count++] = val;
            jskip(p);
            if (p->src[p->pos] == ',') { p->pos++; continue; }
            if (p->src[p->pos] == ']') p->pos++;
            break;
        }
        p->depth--;
        return a;
    }
    if (c == '"') {
        json *s = jnew(JSON_STR);
        s->str = jstring(p);
        if (!s->str) s->str = calloc(1, 1);
        return s;
    }
    if (c == 't' && strncmp(p->src + p->pos, "true", 4) == 0) {
        p->pos += 4;
        json *b = jnew(JSON_BOOL);
        b->boolean = 1;
        return b;
    }
    if (c == 'f' && strncmp(p->src + p->pos, "false", 5) == 0) {
        p->pos += 5;
        return jnew(JSON_BOOL);
    }
    if (c == 'n' && strncmp(p->src + p->pos, "null", 4) == 0) {
        p->pos += 4;
        return jnew(JSON_NULL);
    }
    if (c == '-' || (c >= '0' && c <= '9')) {
        char *end;
        double d = strtod(p->src + p->pos, &end);
        p->pos = (size_t)(end - p->src);
        json *n = jnew(JSON_NUM);
        n->num = d;
        return n;
    }
    return NULL;
}

static json *json_parse(const char *src) {
    jparse p = { src, 0, 0 };
    json *v = jvalue(&p);
    if (!v) return NULL;
    jskip(&p);
    return p.src[p.pos] == '\0' ? v : NULL;
}

static json *obj_get(const json *o, const char *key) {
    if (!o || o->kind != JSON_OBJ) return NULL;
    for (size_t i = 0; i < o->entry_count; i++) {
        if (strcmp(o->keys[i], key) == 0) return o->values[i];
    }
    return NULL;
}

/* ---- validator --------------------------------------------------------- */

/* Raise for larger schemas; the walker saturates. */
#define SOV_MAX_ISSUES 128

typedef enum {
    SOV_ROOT_NOT_OBJECT,
    SOV_MISSING_ADDITIONAL_PROPERTIES,
    SOV_PROPERTY_NOT_REQUIRED,
    SOV_REQUIRED_NOT_PROPERTY,
    SOV_UNSUPPORTED_TYPE,
    SOV_UNSUPPORTED_KEYWORD,
    SOV_INVALID_SCHEMA
} sov_rule;

static const char *sov_rule_name(sov_rule r) {
    switch (r) {
    case SOV_ROOT_NOT_OBJECT: return "root-not-object";
    case SOV_MISSING_ADDITIONAL_PROPERTIES: return "missing-additional-properties";
    case SOV_PROPERTY_NOT_REQUIRED: return "property-not-required";
    case SOV_REQUIRED_NOT_PROPERTY: return "required-not-property";
    case SOV_UNSUPPORTED_TYPE: return "unsupported-type";
    case SOV_UNSUPPORTED_KEYWORD: return "unsupported-keyword";
    default: return "invalid-schema";
    }
}

typedef struct {
    char path[256];
    sov_rule rule;
    char message[320];
} sov_issue;

typedef struct {
    int ok;
    sov_issue issues[SOV_MAX_ISSUES];
    size_t issue_count;
    struct { long objects, properties, enums; } counts;
} sov_report;

static void sov_push(sov_report *rep, const char *path, sov_rule rule, const char *fmt, ...) {
    if (rep->issue_count >= SOV_MAX_ISSUES) return;
    sov_issue *is = &rep->issues[rep->issue_count++];
    snprintf(is->path, sizeof(is->path), "%s", path);
    is->rule = rule;
    va_list ap;
    va_start(ap, fmt);
    vsnprintf(is->message, sizeof(is->message), fmt, ap);
    va_end(ap);
}

static int type_supported(const char *t) {
    static const char *supported[] = { "object", "array", "string", "number", "integer", "boolean" };
    for (size_t i = 0; i < sizeof(supported) / sizeof(supported[0]); i++) {
        if (strcmp(t, supported[i]) == 0) return 1;
    }
    return 0;
}

static int keyword_supported(const char *k) {
    static const char *supported[] = {
        "type", "description", "title", "properties", "required",
        "additionalProperties", "items", "enum", "const", "anyOf", "allOf",
        "$ref", "$defs", "definitions", "format", "nullable", "default",
    };
    for (size_t i = 0; i < sizeof(supported) / sizeof(supported[0]); i++) {
        if (strcmp(k, supported[i]) == 0) return 1;
    }
    return 0;
}

static void walk(sov_report *rep, const json *node, const char *path) {
    char sub[512];

    /* R5 keywords */
    for (size_t i = 0; i < node->entry_count; i++) {
        if (!keyword_supported(node->keys[i])) {
            sov_push(rep, path, SOV_UNSUPPORTED_KEYWORD,
                     "\"%s\" is not supported in strict mode — remove it or express "
                     "the constraint another way.",
                     node->keys[i]);
        }
    }

    /* R5 types ('null' only inside a type array). */
    const json *type = obj_get(node, "type");
    if (type) {
        if (type->kind == JSON_STR) {
            if (!type_supported(type->str)) {
                sov_push(rep, path, SOV_UNSUPPORTED_TYPE,
                         "type \"%s\" is not supported — strict mode allows object, array, "
                         "string, number, integer, boolean (null only inside a type array).",
                         type->str);
            }
        } else if (type->kind == JSON_ARR) {
            for (size_t i = 0; i < type->item_count; i++) {
                const json *t = type->items[i];
                int ok = t->kind == JSON_STR
                    && (type_supported(t->str) || strcmp(t->str, "null") == 0);
                if (!ok) {
                    sov_push(rep, path, SOV_UNSUPPORTED_TYPE,
                             "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 = obj_get(node, "allOf");
    if (all_of && all_of->kind == JSON_ARR && all_of->item_count != 1) {
        sov_push(rep, path, SOV_UNSUPPORTED_KEYWORD,
                 "allOf is supported only with exactly one subschema (use anyOf for unions).");
    }

    /* Object node: R2, R3, R4. */
    int is_object_node = (type && type->kind == JSON_STR && strcmp(type->str, "object") == 0)
        || obj_get(node, "properties") != NULL
        || obj_get(node, "required") != NULL;
    if (is_object_node) {
        rep->counts.objects++;
        const json *ap = obj_get(node, "additionalProperties");
        int ap_false = ap && ap->kind == JSON_BOOL && ap->boolean == 0;
        if (!ap_false) {
            sov_push(rep, path, SOV_MISSING_ADDITIONAL_PROPERTIES,
                     "Object needs \"additionalProperties\": false — strict mode "
                     "rejects open objects.");
        }
        const json *props = obj_get(node, "properties");
        const json *required = obj_get(node, "required");
        if (props && props->kind == JSON_OBJ) {
            rep->counts.properties += (long)props->entry_count;
            for (size_t i = 0; i < props->entry_count; i++) {
                int found = 0;
                if (required && required->kind == JSON_ARR) {
                    for (size_t j = 0; j < required->item_count; j++) {
                        const json *r = required->items[j];
                        if (r->kind == JSON_STR && strcmp(r->str, props->keys[i]) == 0) {
                            found = 1;
                            break;
                        }
                    }
                }
                if (!found) {
                    snprintf(sub, sizeof(sub), "%s.required", path);
                    sov_push(rep, sub, SOV_PROPERTY_NOT_REQUIRED,
                             "\"%s\" is defined in properties but missing from required — "
                             "strict mode requires every property.",
                             props->keys[i]);
                }
            }
        }
        if (required && required->kind == JSON_ARR) {
            for (size_t j = 0; j < required->item_count; j++) {
                const json *r = required->items[j];
                if (r->kind == JSON_STR
                    && (!props || props->kind != JSON_OBJ || !obj_get(props, r->str))) {
                    snprintf(sub, sizeof(sub), "%s.required", path);
                    sov_push(rep, sub, SOV_REQUIRED_NOT_PROPERTY,
                             "\"%s\" is required but has no definition in properties.",
                             r->str);
                }
            }
        }
        if (props && props->kind == JSON_OBJ) {
            for (size_t i = 0; i < props->entry_count; i++) {
                if (props->values[i]->kind == JSON_OBJ) {
                    snprintf(sub, sizeof(sub), "%s.properties.%s", path, props->keys[i]);
                    walk(rep, props->values[i], sub);
                }
            }
        }
    }

    const json *items = obj_get(node, "items");
    if (items && items->kind == JSON_OBJ) {
        snprintf(sub, sizeof(sub), "%s.items", path);
        walk(rep, items, sub);
    }
    const json *enumv = obj_get(node, "enum");
    if (enumv && enumv->kind == JSON_ARR) rep->counts.enums++;

    static const char *lists[] = { "anyOf", "oneOf", "allOf" };
    for (int li = 0; li < 3; li++) {
        const json *list = obj_get(node, lists[li]);
        if (list && list->kind == JSON_ARR) {
            if (li == 1) { /* oneOf */
                snprintf(sub, sizeof(sub), "%s.%s", path, lists[li]);
                sov_push(rep, sub, SOV_UNSUPPORTED_KEYWORD,
                         "oneOf is not supported — strict mode unions are expressed "
                         "with anyOf.");
            }
            for (size_t i = 0; i < list->item_count; i++) {
                if (list->items[i]->kind == JSON_OBJ) {
                    snprintf(sub, sizeof(sub), "%s.%s[%zu]", path, lists[li], i);
                    walk(rep, list->items[i], sub);
                }
            }
        }
    }
    static const char *defs_keys[] = { "$defs", "definitions" };
    for (int di = 0; di < 2; di++) {
        const json *defs = obj_get(node, defs_keys[di]);
        if (defs && defs->kind == JSON_OBJ) {
            for (size_t i = 0; i < defs->entry_count; i++) {
                if (defs->values[i]->kind == JSON_OBJ) {
                    snprintf(sub, sizeof(sub), "%s.%s.%s", path, defs_keys[di], defs->keys[i]);
                    walk(rep, defs->values[i], sub);
                }
            }
        }
    }
}

/** validateStrictSchema: walk the parsed schema. Input must be a JSON
 *  object (call validate_strict_root for the string entry point). */
sov_report sov_validate(const json *schema) {
    sov_report rep = { 1, {0}, 0, {0, 0, 0} };
    if (!schema || schema->kind != JSON_OBJ) {
        sov_push(&rep, "$", SOV_INVALID_SCHEMA, "Schema must be a JSON object.");
        rep.ok = 0;
        return rep;
    }
    walk(&rep, schema, "$");
    rep.ok = rep.issue_count == 0;
    return rep;
}

/** The whole-report entry point: parse + validate + R1 (root must be
 *  type "object"). `input` is a JSON string. */
sov_report sov_validate_root(const char *input) {
    json *schema = json_parse(input);
    if (!schema) {
        sov_report rep = { 0, {0}, 0, {0, 0, 0} };
        sov_push(&rep, "$", SOV_INVALID_SCHEMA, "Not valid JSON.");
        return rep;
    }
    sov_report rep = sov_validate(schema);
    const json *type = obj_get(schema, "type");
    if (!(type && type->kind == JSON_STR && strcmp(type->str, "object") == 0)
        && rep.issue_count < SOV_MAX_ISSUES) {
        sov_issue root = { "$", SOV_ROOT_NOT_OBJECT,
            "The root schema must be type \"object\" — strict mode cannot return "
            "a bare scalar or array." };
        memmove(rep.issues + 1, rep.issues,
                rep.issue_count * sizeof(sov_issue));
        rep.issues[0] = root;
        rep.issue_count++;
        rep.ok = 0;
    }
    return rep;
}

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 →