Strict Output Validator — Java 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 Java 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: Java (17+, zero dependencies)
// 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
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
public final class StrictOutputValidator {
public enum StrictRule {
ROOT_NOT_OBJECT, MISSING_ADDITIONAL_PROPERTIES, PROPERTY_NOT_REQUIRED,
REQUIRED_NOT_PROPERTY, UNSUPPORTED_TYPE, UNSUPPORTED_KEYWORD, INVALID_SCHEMA
}
/** Types strict mode supports. */
private static final Set<String> SUPPORTED_TYPES = Set.of(
"object", "array", "string", "number", "integer", "boolean");
/** Keywords strict mode understands per-node. Everything else is flagged. */
private static final Set<String> SUPPORTED_KEYWORDS = Set.of(
"type", "description", "title", "properties", "required",
"additionalProperties", "items", "enum", "const", "anyOf", "allOf",
"$ref", "$defs", "definitions", "format", "nullable", "default");
public record StrictIssue(String path, StrictRule rule, String message) {}
public record StrictReport(boolean ok, List<StrictIssue> issues,
long objects, long properties, long enums) {}
/** Minimal JSON value tree (the DOM the walker needs). */
public sealed interface J permits JObj, JArr, JStr, JNum, JBool, JNull {}
public record JObj(java.util.LinkedHashMap<String, J> entries) implements J {}
public record JArr(List<J> items) implements J {}
public record JStr(String value) implements J {}
public record JNum(double value) implements J {}
public record JBool(boolean value) implements J {}
public record JNull() implements J {}
/**
* validateStrictSchema: walk the parsed schema and collect rule issues.
*/
public static StrictReport validateStrictSchema(J schema) {
List<StrictIssue> issues = new ArrayList<>();
long[] counts = {0, 0, 0}; // objects, properties, enums
if (!(schema instanceof JObj root)) {
issues.add(new StrictIssue("$", StrictRule.INVALID_SCHEMA,
"Schema must be a JSON object."));
return new StrictReport(false, issues, 0, 0, 0);
}
walk(root, "$", issues, counts);
return new StrictReport(issues.isEmpty(), issues, counts[0], counts[1], counts[2]);
}
private static void walk(JObj node, String path, List<StrictIssue> issues, long[] counts) {
// R5 keywords
for (String key : node.entries().keySet()) {
if (!SUPPORTED_KEYWORDS.contains(key)) {
issues.add(new StrictIssue(path, StrictRule.UNSUPPORTED_KEYWORD,
"\"" + key + "\" is not supported in strict mode — remove it or "
+ "express the constraint another way."));
}
}
// R5 types ('null' only inside a type array).
J typeNode = node.entries().get("type");
if (typeNode instanceof JStr typeStr) {
if (!SUPPORTED_TYPES.contains(typeStr.value())) {
issues.add(new StrictIssue(path, StrictRule.UNSUPPORTED_TYPE,
"type \"" + typeStr.value() + "\" is not supported — strict mode "
+ "allows object, array, string, number, integer, boolean "
+ "(null only inside a type array)."));
}
} else if (typeNode instanceof JArr typeArr) {
for (J t : typeArr.items()) {
boolean ok = t instanceof JStr s
&& (SUPPORTED_TYPES.contains(s.value()) || s.value().equals("null"));
if (!ok) {
issues.add(new StrictIssue(path, StrictRule.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.
J allOf = node.entries().get("allOf");
if (allOf instanceof JArr arr && arr.items().size() != 1) {
issues.add(new StrictIssue(path, StrictRule.UNSUPPORTED_KEYWORD,
"allOf is supported only with exactly one subschema (use anyOf for unions)."));
}
// Object node: R2, R3, R4.
boolean isObjectNode =
(typeNode instanceof JStr t && t.value().equals("object"))
|| node.entries().containsKey("properties")
|| node.entries().containsKey("required");
if (isObjectNode) {
counts[0]++;
J ap = node.entries().get("additionalProperties");
boolean apFalse = ap instanceof JBool b && !b.value();
if (!apFalse) {
issues.add(new StrictIssue(path, StrictRule.MISSING_ADDITIONAL_PROPERTIES,
"Object needs \"additionalProperties\": false — strict mode "
+ "rejects open objects."));
}
JObj props = node.entries().get("properties") instanceof JObj p ? p : null;
JArr required = node.entries().get("required") instanceof JArr r ? r : null;
Set<String> reqKeys = new HashSet<>();
if (required != null) {
for (J r : required.items()) {
if (r instanceof JStr s) reqKeys.add(s.value());
}
}
if (props != null) {
counts[1] += props.entries().size();
for (String key : props.entries().keySet()) {
if (!reqKeys.contains(key)) {
issues.add(new StrictIssue(path + ".required",
StrictRule.PROPERTY_NOT_REQUIRED,
"\"" + key + "\" is defined in properties but missing from "
+ "required — strict mode requires every property."));
}
}
for (String key : reqKeys) {
if (!props.entries().containsKey(key)) {
issues.add(new StrictIssue(path + ".required",
StrictRule.REQUIRED_NOT_PROPERTY,
"\"" + key + "\" is required but has no definition in properties."));
}
}
for (var e : props.entries().entrySet()) {
if (e.getValue() instanceof JObj sub) {
walk(sub, path + ".properties." + e.getKey(), issues, counts);
}
}
} else {
for (String key : reqKeys) {
issues.add(new StrictIssue(path + ".required",
StrictRule.REQUIRED_NOT_PROPERTY,
"\"" + key + "\" is required but has no definition in properties."));
}
}
}
J items = node.entries().get("items");
if (items instanceof JObj sub) walk(sub, path + ".items", issues, counts);
if (node.entries().get("enum") instanceof JArr) counts[2]++;
String[] lists = {"anyOf", "oneOf", "allOf"};
for (String listKey : lists) {
if (node.entries().get(listKey) instanceof JArr list) {
if (listKey.equals("oneOf")) {
issues.add(new StrictIssue(path + "." + listKey,
StrictRule.UNSUPPORTED_KEYWORD,
"oneOf is not supported — strict mode unions are expressed "
+ "with anyOf."));
}
for (int i = 0; i < list.items().size(); i++) {
if (list.items().get(i) instanceof JObj sub) {
walk(sub, path + "." + listKey + "[" + i + "]", issues, counts);
}
}
}
}
String[] defsKeys = {"$defs", "definitions"};
for (String defsKey : defsKeys) {
if (node.entries().get(defsKey) instanceof JObj defs) {
for (var e : defs.entries().entrySet()) {
if (e.getValue() instanceof JObj sub) {
walk(sub, path + "." + defsKey + "." + e.getKey(), issues, counts);
}
}
}
}
}
/**
* The whole-report entry point: parse + validate + R1 (root must be
* type "object"). Input is a JSON string; a sibling parser builds the J
* tree (any JSON library maps to the DOM above in a few lines).
*/
public static StrictReport validateStrictRoot(String input, java.util.function.Function<String, J> parse) {
J schema;
try {
schema = parse.apply(input);
} catch (RuntimeException e) {
return new StrictReport(false, List.of(new StrictIssue("$",
StrictRule.INVALID_SCHEMA, "Not valid JSON: " + e.getMessage())), 0, 0, 0);
}
StrictReport report = validateStrictSchema(schema);
if (schema instanceof JObj root && !(root.entries().get("type") instanceof JStr t
&& t.value().equals("object"))) {
List<StrictIssue> issues = new ArrayList<>();
issues.add(new StrictIssue("$", StrictRule.ROOT_NOT_OBJECT,
"The root schema must be type \"object\" — strict mode cannot return "
+ "a bare scalar or array."));
issues.addAll(report.issues());
return new StrictReport(false, issues, report.objects(), report.properties(),
report.enums());
}
return report;
}
private StrictOutputValidator() {}
}
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 →