Skip to content

Strict Output Validator — Swift 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 Swift 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: Swift (5.9+, Foundation for JSONSerialization only)
// 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 Foundation

enum StrictRule: String {
    case rootNotObject = "root-not-object"
    case missingAdditionalProperties = "missing-additional-properties"
    case propertyNotRequired = "property-not-required"
    case requiredNotProperty = "required-not-property"
    case unsupportedType = "unsupported-type"
    case unsupportedKeyword = "unsupported-keyword"
    case invalidSchema = "invalid-schema"
}

struct StrictIssue {
    var path: String
    var rule: StrictRule
    var message: String
}

struct StrictReport {
    var ok = true
    var issues: [StrictIssue] = []
    var objects = 0
    var properties = 0
    var enums = 0
}

/// Types strict mode supports.
let supportedTypes: Set<String> = [
    "object", "array", "string", "number", "integer", "boolean",
]

/// Keywords strict mode understands per-node. Everything else is flagged.
let supportedKeywords: Set<String> = [
    "type", "description", "title", "properties", "required",
    "additionalProperties", "items", "enum", "const", "anyOf", "allOf",
    "$ref", "$defs", "definitions", "format", "nullable", "default",
]

/// validateStrictSchema over a JSONSerialization tree (Any_dictionary).
func validateStrictSchema(_ schema: Any) -> StrictReport {
    var report = StrictReport()

    func isObj(_ v: Any?) -> Bool {
        v is [String: Any]
    }

    guard let root = schema as? [String: Any] else {
        report.issues.append(StrictIssue(
            path: "$", rule: .invalidSchema, message: "Schema must be a JSON object."))
        report.ok = false
        return report
    }

    func walk(_ node: [String: Any], _ path: String) {
        // R5 keywords
        for key in node.keys where !supportedKeywords.contains(key) {
            report.issues.append(StrictIssue(
                path: path, rule: .unsupportedKeyword,
                message: "\"\(key)\" is not supported in strict mode — remove it or " +
                    "express the constraint another way."))
        }

        // R5 types ('null' only inside a type array).
        if let type = node["type"] as? String {
            if !supportedTypes.contains(type) {
                report.issues.append(StrictIssue(
                    path: path, rule: .unsupportedType,
                    message: "type \"\(type)\" is not supported — strict mode allows object, " +
                        "array, string, number, integer, boolean (null only inside a type array)."))
            }
        } else if let typeArr = node["type"] as? [Any] {
            for t in typeArr {
                let ok = (t as? String).map { supportedTypes.contains($0) || $0 == "null" } ?? false
                if !ok {
                    report.issues.append(StrictIssue(
                        path: path, rule: .unsupportedType,
                        message: "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 let allOf = node["allOf"] as? [Any], allOf.count != 1 {
            report.issues.append(StrictIssue(
                path: path, rule: .unsupportedKeyword,
                message: "allOf is supported only with exactly one subschema " +
                    "(use anyOf for unions)."))
        }

        // Object node: R2, R3, R4.
        let typeStr = node["type"] as? String
        let isObjectNode = typeStr == "object"
            || node["properties"] != nil
            || node["required"] != nil
        if isObjectNode {
            report.objects += 1
            let apFalse = (node["additionalProperties"] as? Bool) == false
            if !apFalse {
                report.issues.append(StrictIssue(
                    path: path, rule: .missingAdditionalProperties,
                    message: "Object needs \"additionalProperties\": false — strict mode " +
                        "rejects open objects."))
            }
            let props = node["properties"] as? [String: Any]
            let required = (node["required"] as? [Any])?.compactMap { $0 as? String } ?? []
            let reqKeys = Set(required)
            if let props = props {
                report.properties += props.count
                for key in props.keys where !reqKeys.contains(key) {
                    report.issues.append(StrictIssue(
                        path: path + ".required", rule: .propertyNotRequired,
                        message: "\"\(key)\" is defined in properties but missing from " +
                            "required — strict mode requires every property."))
                }
                for key in reqKeys where props[key] == nil {
                    report.issues.append(StrictIssue(
                        path: path + ".required", rule: .requiredNotProperty,
                        message: "\"\(key)\" is required but has no definition in properties."))
                }
                for (key, sub) in props {
                    if let subObj = sub as? [String: Any] {
                        walk(subObj, "\(path).properties.\(key)")
                    }
                }
            } else {
                for key in reqKeys {
                    report.issues.append(StrictIssue(
                        path: path + ".required", rule: .requiredNotProperty,
                        message: "\"\(key)\" is required but has no definition in properties."))
                }
            }
        }

        if let items = node["items"] as? [String: Any] {
            walk(items, path + ".items")
        }
        if node["enum"] is [Any] { report.enums += 1 }

        for listKey in ["anyOf", "oneOf", "allOf"] {
            guard let list = node[listKey] as? [Any] else { continue }
            if listKey == "oneOf" {
                report.issues.append(StrictIssue(
                    path: path + "." + listKey, rule: .unsupportedKeyword,
                    message: "oneOf is not supported — strict mode unions are expressed " +
                        "with anyOf."))
            }
            for (i, sub) in list.enumerated() {
                if let subObj = sub as? [String: Any] {
                    walk(subObj, "\(path).\(listKey)[\(i)]")
                }
            }
        }
        for defsKey in ["$defs", "definitions"] {
            guard let defs = node[defsKey] as? [String: Any] else { continue }
            for (name, sub) in defs {
                if let subObj = sub as? [String: Any] {
                    walk(subObj, "\(path).\(defsKey).\(name)")
                }
            }
        }
    }

    walk(root, "$")
    report.ok = report.issues.isEmpty
    return report
}

/// The whole-report entry point: parse + validate + R1 (root must be
/// type "object"). Input is a JSON string.
func validateStrictRoot(_ input: String) -> StrictReport {
    guard let data = input.data(using: .utf8),
          let parsed = try? JSONSerialization.jsonObject(with: data) else {
        var report = StrictReport()
        report.ok = false
        report.issues.append(StrictIssue(
            path: "$", rule: .invalidSchema, message: "Not valid JSON."))
        return report
    }
    var report = validateStrictSchema(parsed)
    if let root = parsed as? [String: Any], root["type"] as? String != "object" {
        report.issues.insert(StrictIssue(
            path: "$", rule: .rootNotObject,
            message: "The root schema must be type \"object\" — strict mode cannot return " +
                "a bare scalar or array."), at: 0)
        report.ok = false
    }
    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 →