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 →