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 →