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# (.NET 8, zero dependencies beyond System.Text.Json)
// 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
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.Json;
using System.Text.Json.Nodes;
namespace CosmoDev.StrictOutputValidator;
public enum StrictRule
{
RootNotObject,
MissingAdditionalProperties,
PropertyNotRequired,
RequiredNotProperty,
UnsupportedType,
UnsupportedKeyword,
InvalidSchema,
}
public sealed record StrictIssue(string Path, StrictRule Rule, string Message);
public sealed record StrictReport(
bool Ok,
IReadOnlyList<StrictIssue> Issues,
long Objects,
long Properties,
long Enums);
public static class StrictOutputValidator
{
private static readonly HashSet<string> SupportedTypes =
new() { "object", "array", "string", "number", "integer", "boolean" };
private static readonly HashSet<string> SupportedKeywords = new()
{
"type", "description", "title", "properties", "required",
"additionalProperties", "items", "enum", "const", "anyOf", "allOf",
"$ref", "$defs", "definitions", "format", "nullable", "default",
};
/// <summary>
/// Validate a parsed JSON Schema node. Non-object input reports
/// <c>invalid-schema</c> (the TS contract).
/// </summary>
public static StrictReport ValidateStrictSchema(JsonNode? schema)
{
var issues = new List<StrictIssue>();
long objects = 0, properties = 0, enums = 0;
if (schema is not JsonObject root)
{
issues.Add(new StrictIssue("$", StrictRule.InvalidSchema,
"Schema must be a JSON object."));
return new StrictReport(false, issues, 0, 0, 0);
}
Walk(root, "$");
return new StrictReport(issues.Count == 0, issues, objects, properties, enums);
void Walk(JsonObject node, string path)
{
// R5 keywords
foreach (var key in node.Select(kv => kv.Key))
{
if (!SupportedKeywords.Contains(key))
{
issues.Add(new StrictIssue(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).
if (node["type"] is { } typeNode)
{
if (typeNode is JsonValue tv && tv.TryGetValue<string>(out var typeStr))
{
if (!SupportedTypes.Contains(typeStr))
{
issues.Add(new StrictIssue(path, StrictRule.UnsupportedType,
$"type \"{typeStr}\" is not supported — strict mode allows object, " +
"array, string, number, integer, boolean (null only inside a type array)."));
}
}
else if (typeNode is JsonArray typeArr)
{
foreach (var t in typeArr)
{
var s = (t as JsonValue)?.GetValue<string>();
var ok = s != null && (SupportedTypes.Contains(s) || s == "null");
if (!ok)
{
issues.Add(new StrictIssue(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.
if (node["allOf"] is JsonArray allOf && allOf.Count != 1)
{
issues.Add(new StrictIssue(path, StrictRule.UnsupportedKeyword,
"allOf is supported only with exactly one subschema (use anyOf for unions)."));
}
// Object node: R2, R3, R4.
var typeValue = (node["type"] as JsonValue)?.GetValue<string>();
bool isObjectNode = typeValue == "object"
|| node.ContainsKey("properties")
|| node.ContainsKey("required");
if (isObjectNode)
{
objects++;
var ap = node["additionalProperties"];
bool apFalse = ap is JsonValue b && b.TryGetValue<bool>(out var bv) && !bv;
if (!apFalse)
{
issues.Add(new StrictIssue(path, StrictRule.MissingAdditionalProperties,
"Object needs \"additionalProperties\": false — strict mode rejects " +
"open objects."));
}
var props = node["properties"] as JsonObject;
var required = node["required"] as JsonArray;
var reqKeys = required?.OfType<JsonValue>()
.Where(r => r.TryGetValue<string>(out _))
.Select(r => r.GetValue<string>())
.ToHashSet() ?? new HashSet<string>();
if (props != null)
{
properties += props.Count;
foreach (var key in props.Select(kv => kv.Key))
{
if (!reqKeys.Contains(key))
{
issues.Add(new StrictIssue(path + ".required",
StrictRule.PropertyNotRequired,
$"\"{key}\" is defined in properties but missing from " +
"required — strict mode requires every property."));
}
}
foreach (var key in reqKeys)
{
if (!props.ContainsKey(key))
{
issues.Add(new StrictIssue(path + ".required",
StrictRule.RequiredNotProperty,
$"\"{key}\" is required but has no definition in properties."));
}
}
foreach (var (key, sub) in props)
{
if (sub is JsonObject o) Walk(o, $"{path}.properties.{key}");
}
}
else
{
foreach (var key in reqKeys)
{
issues.Add(new StrictIssue(path + ".required",
StrictRule.RequiredNotProperty,
$"\"{key}\" is required but has no definition in properties."));
}
}
}
if (node["items"] is JsonObject items) Walk(items, path + ".items");
if (node["enum"] is JsonArray) enums++;
foreach (var listKey in new[] { "anyOf", "oneOf", "allOf" })
{
if (node[listKey] is JsonArray list)
{
if (listKey == "oneOf")
{
issues.Add(new StrictIssue(path + "." + listKey,
StrictRule.UnsupportedKeyword,
"oneOf is not supported — strict mode unions are expressed with anyOf."));
}
for (int i = 0; i < list.Count; i++)
{
if (list[i] is JsonObject sub)
{
Walk(sub, $"{path}.{listKey}[{i}]");
}
}
}
}
foreach (var defsKey in new[] { "$defs", "definitions" })
{
if (node[defsKey] is JsonObject defs)
{
foreach (var (name, sub) in defs)
{
if (sub is JsonObject o) Walk(o, $"{path}.{defsKey}.{name}");
}
}
}
}
}
/// <summary>
/// The whole-report entry point: parse + validate + R1 (root must be
/// type "object"). Input is a JSON string.
/// </summary>
public static StrictReport ValidateStrictRoot(string input)
{
JsonNode? schema;
try
{
schema = JsonNode.Parse(input);
}
catch (JsonException e)
{
return new StrictReport(false,
new[] { new StrictIssue("$", StrictRule.InvalidSchema,
$"Not valid JSON: {e.Message}") }, 0, 0, 0);
}
var report = ValidateStrictSchema(schema);
if (schema is JsonObject root
&& !((root["type"] as JsonValue)?.GetValue<string>() == "object"))
{
var issues = new List<StrictIssue>
{
new("$", StrictRule.RootNotObject,
"The root schema must be type \"object\" — strict mode cannot return " +
"a bare scalar or array."),
};
issues.AddRange(report.Issues);
return new StrictReport(false, issues, report.Objects, report.Properties,
report.Enums);
}
return report;
}
}
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 →