Strict Output Validator — Zig 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 Zig 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: Zig (0.13+, stdlib only — std.json supplies the DOM)
// Port of src/lib/strictOutputValidator.ts (the canonical TypeScript
// implementation).
// 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
const std = @import("std");
pub const Rule = enum {
root_not_object,
missing_additional_properties,
property_not_required,
required_not_property,
unsupported_type,
unsupported_keyword,
invalid_schema,
pub fn name(self: Rule) []const u8 {
return switch (self) {
.root_not_object => "root-not-object",
.missing_additional_properties => "missing-additional-properties",
.property_not_required => "property-not-required",
.required_not_property => "required-not-property",
.unsupported_type => "unsupported-type",
.unsupported_keyword => "unsupported-keyword",
.invalid_schema => "invalid-schema",
};
}
};
/// Raise for larger schemas; the walker saturates.
pub const MAX_ISSUES = 128;
pub const Issue = struct {
path: []const u8, // allocated
rule: Rule,
message: []const u8, // allocated
};
pub const Report = struct {
ok: bool = true,
issues: []Issue = &.{},
objects: i64 = 0,
properties: i64 = 0,
enums: i64 = 0,
pub fn free(self: *Report, alloc: std.mem.Allocator) void {
for (self.issues) |is| {
alloc.free(is.path);
alloc.free(is.message);
}
alloc.free(self.issues);
self.issues = &.{};
}
};
fn typeSupported(t: []const u8) bool {
const supported = [_][]const u8{
"object", "array", "string", "number", "integer", "boolean",
};
for (supported) |s| {
if (std.mem.eql(u8, t, s)) return true;
}
return false;
}
fn keywordSupported(k: []const u8) bool {
const supported = [_][]const u8{
"type", "description", "title", "properties",
"required", "additionalProperties", "items", "enum",
"const", "anyOf", "allOf", "$ref",
"$defs", "definitions", "format", "nullable",
"default",
};
for (supported) |s| {
if (std.mem.eql(u8, k, s)) return true;
}
return false;
}
const Walker = struct {
alloc: std.mem.Allocator,
issues: std.ArrayList(Issue),
objects: i64 = 0,
properties: i64 = 0,
enums: i64 = 0,
const ObjMap = std.StringArrayHashMap(std.json.Value);
fn push(self: *Walker, path: []const u8, rule: Rule, comptime fmt: []const u8, args: anytype) !void {
if (self.issues.items.len >= MAX_ISSUES) return;
try self.issues.append(.{
.path = try self.alloc.dupe(u8, path),
.rule = rule,
.message = try std.fmt.allocPrint(self.alloc, fmt, args),
});
}
fn objOf(value: std.json.Value) ?*ObjMap {
return switch (value) {
.object => |*o| o,
else => null,
};
}
fn strOf(value: std.json.Value) ?[]const u8 {
return switch (value) {
.string => |s| s,
else => null,
};
}
fn boolOf(value: std.json.Value) ?bool {
return switch (value) {
.bool => |b| b,
else => null,
};
}
fn subPath(self: *Walker, path: []const u8, comptime fmt: []const u8, args: anytype) ![]const u8 {
return std.fmt.allocPrint(self.alloc, "{s}" ++ fmt, .{path} ++ args);
}
fn walk(self: *Walker, node: *ObjMap, path: []const u8) !void {
// R5 keywords
for (node.keys()) |key| {
if (!keywordSupported(key)) {
try self.push(path, .unsupported_keyword,
"\"{s}\" is not supported in strict mode — remove it or express the constraint another way.", .{key});
}
}
// R5 types ('null' only inside a type array).
if (node.get("type")) |type_value| {
switch (type_value) {
.string => |t| {
if (!typeSupported(t)) {
try self.push(path, .unsupported_type,
"type \"{s}\" is not supported — strict mode allows object, array, string, number, integer, boolean (null only inside a type array).", .{t});
}
},
.array => |arr| {
for (arr.items) |t| {
const s = strOf(t) orelse "";
const ok = strOf(t) != null and (typeSupported(s) or std.mem.eql(u8, s, "null"));
if (!ok) {
try self.push(path, .unsupported_type,
"a type-array entry is not supported — strict mode allows object, array, string, number, integer, boolean, null.", .{});
}
}
},
else => {},
}
}
// allOf is accepted only as a single-element wrapper.
if (node.get("allOf")) |all_of| {
if (all_of == .array and all_of.array.items.len != 1) {
try self.push(path, .unsupported_keyword,
"allOf is supported only with exactly one subschema (use anyOf for unions).", .{});
}
}
// Object node: R2, R3, R4.
const type_str = if (node.get("type")) |t| strOf(t) else null;
const is_object_node = (type_str != null and std.mem.eql(u8, type_str.?, "object"))
or node.get("properties") != null
or node.get("required") != null;
if (is_object_node) {
self.objects += 1;
const ap = node.get("additionalProperties");
const ap_false = ap != null and boolOf(ap.?) == false;
if (!ap_false) {
try self.push(path, .missing_additional_properties,
"Object needs \"additionalProperties\": false — strict mode rejects open objects.", .{});
}
const props: ?*ObjMap = if (node.get("properties")) |p| objOf(p) else null;
var req_keys: std.ArrayList([]const u8) = .init(self.alloc);
defer req_keys.deinit();
if (node.get("required")) |r| {
if (r == .array) {
for (r.array.items) |item| {
if (strOf(item)) |s| try req_keys.append(s);
}
}
}
if (props) |p| {
self.properties += @intCast(p.count());
for (p.keys()) |key| {
var found = false;
for (req_keys.items) |rk| {
if (std.mem.eql(u8, rk, key)) {
found = true;
break;
}
}
if (!found) {
const sub = try self.subPath(path, ".required", .{});
defer self.alloc.free(sub);
try self.push(path, .property_not_required,
"\"{s}\" is defined in properties but missing from required — strict mode requires every property.", .{key});
}
}
for (req_keys.items) |rk| {
if (p.get(rk) == null) {
try self.push(path, .required_not_property,
"\"{s}\" is required but has no definition in properties.", .{rk});
}
}
var it = p.iterator();
while (it.next()) |entry| {
if (objOf(entry.value_ptr.*)) |sub| {
const sp = try self.subPath(path, ".properties.{s}", .{entry.key_ptr.*});
try self.walk(sub, sp);
}
}
} else {
for (req_keys.items) |rk| {
try self.push(path, .required_not_property,
"\"{s}\" is required but has no definition in properties.", .{rk});
}
}
}
if (node.get("items")) |items| {
if (objOf(items)) |sub| {
const sp = try self.subPath(path, ".items", .{});
try self.walk(sub, sp);
}
}
if (node.get("enum")) |e| {
if (e == .array) self.enums += 1;
}
inline for (.{ "anyOf", "oneOf", "allOf" }) |list_key| {
if (node.get(list_key)) |list| {
if (list == .array) {
if (comptime std.mem.eql(u8, list_key, "oneOf")) {
try self.push(path, .unsupported_keyword,
"oneOf is not supported — strict mode unions are expressed with anyOf.", .{});
}
for (list.array.items, 0..) |sub_value, i| {
if (objOf(sub_value)) |sub| {
const sp = try self.subPath(path, "." ++ list_key ++ "[{d}]", .{i});
try self.walk(sub, sp);
}
}
}
}
}
inline for (.{ "$defs", "definitions" }) |defs_key| {
if (node.get(defs_key)) |defs| {
if (objOf(defs)) |d| {
var it = d.iterator();
while (it.next()) |entry| {
if (objOf(entry.value_ptr.*)) |sub| {
const sp = try self.subPath(path, "." ++ defs_key ++ ".{s}", .{entry.key_ptr.*});
try self.walk(sub, sp);
}
}
}
}
}
}
};
/// validateStrictSchema over a parsed std.json object.
pub fn validateStrictSchema(alloc: std.mem.Allocator, schema: std.json.Value) !Report {
var w: Walker = .{ .alloc = alloc, .issues = .init(alloc) };
errdefer {
for (w.issues.items) |is| {
alloc.free(is.path);
alloc.free(is.message);
}
w.issues.deinit();
}
const root = Walker.objOf(schema) orelse {
try w.push("$", .invalid_schema, "Schema must be a JSON object.", .{});
var rep: Report = .{ .ok = false };
rep.issues = try w.issues.toOwnedSlice();
return rep;
};
try w.walk(root, "$");
var rep: Report = .{
.ok = w.issues.items.len == 0,
.objects = w.objects,
.properties = w.properties,
.enums = w.enums,
};
rep.issues = try w.issues.toOwnedSlice();
return rep;
}
/// The whole-report entry point: parse + validate + R1 (root must be
/// type "object"). `input` is a JSON string.
pub fn validateStrictRoot(alloc: std.mem.Allocator, input: []const u8) !Report {
const parsed = std.json.parseFromSlice(std.json.Value, alloc, input, .{}) catch {
var w: Walker = .{ .alloc = alloc, .issues = .init(alloc) };
try w.push("$", .invalid_schema, "Not valid JSON.", .{});
var rep: Report = .{ .ok = false };
rep.issues = try w.issues.toOwnedSlice();
return rep;
};
defer parsed.deinit();
var rep = try validateStrictSchema(alloc, parsed.value);
if (Walker.objOf(parsed.value)) |root| {
const t = root.get("type");
const is_object = t != null and Walker.strOf(t.?) != null
and std.mem.eql(u8, Walker.strOf(t.?).?, "object");
if (!is_object) {
var issues: std.ArrayList(Issue) = .init(alloc);
defer issues.deinit();
try issues.append(.{
.path = try alloc.dupe(u8, "$"),
.rule = .root_not_object,
.message = try alloc.dupe(u8,
"The root schema must be type \"object\" — strict mode cannot return a bare scalar or array."),
});
try issues.appendSlice(rep.issues);
alloc.free(rep.issues);
rep.issues = try issues.toOwnedSlice();
rep.ok = false;
}
}
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 →