Skip to content

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 →