diff --git a/.gitignore b/.gitignore index 0023a53..f129058 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,4 @@ DerivedData/ .swiftpm/configuration/registries.json .swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata .netrc +Package.resolved \ No newline at end of file diff --git a/Package.swift b/Package.swift index 05757e5..2b91f64 100644 --- a/Package.swift +++ b/Package.swift @@ -9,13 +9,19 @@ let package = Package( // Products define the executables and libraries a package produces, making them visible to other packages. .library( name: "JSONSchema", - targets: ["JSONSchema"]), + targets: ["JSONSchema"]) + ], + dependencies: [ + .package(url: "https://github.com/apple/swift-collections.git", from: "1.0.0") ], targets: [ // Targets are the basic building blocks of a package, defining a module or a test suite. // Targets can depend on other targets in this package and products from dependencies. .target( - name: "JSONSchema"), + name: "JSONSchema", + dependencies: [ + .product(name: "OrderedCollections", package: "swift-collections") + ]), .testTarget( name: "JSONSchemaTests", dependencies: ["JSONSchema"] diff --git a/Sources/JSONSchema/JSONSchema.swift b/Sources/JSONSchema/JSONSchema.swift index 74a735d..52cf9e7 100644 --- a/Sources/JSONSchema/JSONSchema.swift +++ b/Sources/JSONSchema/JSONSchema.swift @@ -1,3 +1,6 @@ +import Foundation +import OrderedCollections + /// A type that represents a JSON Schema definition. /// /// Use JSONSchema to create, manipulate, and encode/decode JSON Schema documents. @@ -41,7 +44,7 @@ enum: [JSONValue]? = nil, const: JSONValue? = nil, /* */ - properties: [String: JSONSchema] = [:], + properties: OrderedDictionary = [:], required: [String] = [], additionalProperties: AdditionalProperties? = nil ) @@ -677,7 +680,8 @@ extension JSONSchema: Codable { switch type { case "object": let properties = - try container.decodeIfPresent([String: JSONSchema].self, forKey: .properties) ?? [:] + try container.decodeIfPresent( + OrderedDictionary.self, forKey: .properties) ?? [:] let required = try container.decodeIfPresent([String].self, forKey: .required) ?? [] let additionalProperties = try container.decodeIfPresent( AdditionalProperties.self, forKey: .additionalProperties) diff --git a/Tests/JSONSchemaTests/JSONSchemaTests.swift b/Tests/JSONSchemaTests/JSONSchemaTests.swift index 0df57ab..9edc1f7 100644 --- a/Tests/JSONSchemaTests/JSONSchemaTests.swift +++ b/Tests/JSONSchemaTests/JSONSchemaTests.swift @@ -3,469 +3,450 @@ import Testing @testable import JSONSchema -@Test func testObjectSchema() throws { - let schema: JSONSchema = .object( - title: "Person", - description: "A person schema", - default: ["name": "John Doe"], - examples: [["name": "Jane Doe", "age": 25]], - enum: [["name": "Option 1"], ["name": "Option 2"]], - const: ["type": "constant"], - properties: [ - "name": .string(), - "age": .integer(minimum: 0, maximum: 120), - "address": .object( - properties: [ - "street": .string(), - "city": .string(), - "zip": .string(pattern: "^[0-9]{5}$"), - ] - ), - ], - required: ["name", "age"], - additionalProperties: .boolean(false) - ) - +// Helper function to compare JSON schemas by their sorted JSON representation +func assertJSONSchemaEquivalent( + _ schema1: JSONSchema, _ schema2: JSONSchema, fileID: String = #fileID, + filePath: String = #filePath, line: Int = #line, column: Int = #column +) throws { let encoder = JSONEncoder() - encoder.outputFormatting = [.prettyPrinted, .sortedKeys] - let data = try encoder.encode(schema) - - let decoder = JSONDecoder() - let decodedSchema = try decoder.decode(JSONSchema.self, from: data) - - #expect(decodedSchema == schema) - - // Verify properties were encoded and decoded correctly - if case let .object( - title, description, defaultValue, examples, enumValue, constValue, properties, required, - additionalProperties) = decodedSchema - { - #expect(title == "Person") - #expect(description == "A person schema") - #expect(defaultValue?.objectValue?["name"]?.stringValue == "John Doe") - #expect(examples?.count == 1) - #expect(enumValue?.count == 2) - #expect(constValue?.objectValue?["type"]?.stringValue == "constant") - #expect(properties.count == 3) - #expect(required.count == 2) - #expect(required.contains("name")) - #expect(required.contains("age")) - - if case let .boolean(value) = additionalProperties { - #expect(value == false) + encoder.outputFormatting = [.sortedKeys, .prettyPrinted] + + let data1 = try encoder.encode(schema1) + let data2 = try encoder.encode(schema2) + + let json1 = String(data: data1, encoding: .utf8)! + let json2 = String(data: data2, encoding: .utf8)! + + #expect( + json1 == json2, + sourceLocation: SourceLocation( + fileID: fileID, filePath: filePath, line: line, column: column)) +} + +@Suite("JSONSchema Tests") +struct JSONSchemaTests { + @Test func testObjectSchema() throws { + let schema: JSONSchema = .object( + title: "Person", + description: "A person schema", + default: ["name": "John Doe"], + examples: [["name": "Jane Doe", "age": 25]], + enum: [["name": "Option 1"], ["name": "Option 2"]], + const: ["type": "constant"], + properties: [ + "name": .string(), + "age": .integer(minimum: 0, maximum: 120), + "address": .object( + properties: [ + "street": .string(), + "city": .string(), + "zip": .string(pattern: "^[0-9]{5}$"), + ] + ), + ], + required: ["name", "age"], + additionalProperties: .boolean(false) + ) + + let encoder = JSONEncoder() + encoder.outputFormatting = [.prettyPrinted] + let data = try encoder.encode(schema) + + let decoder = JSONDecoder() + let decodedSchema = try decoder.decode(JSONSchema.self, from: data) + + // Verify schemas are equivalent using sorted JSON comparison + try assertJSONSchemaEquivalent(decodedSchema, schema) + + // Verify properties were encoded and decoded correctly + if case let .object( + title, description, defaultValue, examples, enumValue, constValue, properties, required, + additionalProperties) = decodedSchema + { + #expect(title == "Person") + #expect(description == "A person schema") + #expect(defaultValue?.objectValue?["name"]?.stringValue == "John Doe") + #expect(examples?.count == 1) + #expect(enumValue?.count == 2) + #expect(constValue?.objectValue?["type"]?.stringValue == "constant") + #expect(properties.count == 3) + #expect(required.count == 2) + #expect(required.contains("name")) + #expect(required.contains("age")) + + if case let .boolean(value) = additionalProperties { + #expect(value == false) + } else { + Issue.record("additionalProperties should be .boolean(false)") + } } else { - Issue.record("additionalProperties should be .boolean(false)") + Issue.record("Decoded schema should be an object schema") } - } else { - Issue.record("Decoded schema should be an object schema") } -} -@Test func testObjectSchemaWithLiteral() throws { - let schema: JSONSchema = [ - "name": .string(), - "age": .integer(minimum: 0, maximum: 120), - "address": [ - "street": .string(), - "city": .string(), - ], - "zip": .string(pattern: "^[0-9]{5}$"), - ] + @Test func testObjectSchemaWithLiteral() throws { + let schema: JSONSchema = [ + "name": .string(), + "age": .integer(minimum: 0, maximum: 120), + "address": [ + "street": .string(), + "city": .string(), + ], + "zip": .string(pattern: "^[0-9]{5}$"), + ] - let encoder = JSONEncoder() - encoder.outputFormatting = [.prettyPrinted, .sortedKeys] - let data = try encoder.encode(schema) - - let decoder = JSONDecoder() - let decodedSchema = try decoder.decode(JSONSchema.self, from: data) - - #expect(decodedSchema == schema) - - // Verify properties were encoded and decoded correctly - if case let .object( - title, description, defaultValue, examples, enumValue, constValue, properties, required, - additionalProperties) = decodedSchema - { - #expect(title == nil) - #expect(description == nil) - #expect(defaultValue == nil) - #expect(examples == nil) - #expect(enumValue == nil) - #expect(constValue == nil) - #expect(properties.count == 4) - #expect(required == []) - #expect(additionalProperties == nil) + let encoder = JSONEncoder() + encoder.outputFormatting = [.prettyPrinted] + let data = try encoder.encode(schema) + + let decoder = JSONDecoder() + let decodedSchema = try decoder.decode(JSONSchema.self, from: data) + + // Verify schemas are equivalent using sorted JSON comparison + try assertJSONSchemaEquivalent(decodedSchema, schema) + + // Verify properties were encoded and decoded correctly + if case let .object( + title, description, defaultValue, examples, enumValue, constValue, properties, required, + additionalProperties) = decodedSchema + { + #expect(title == nil) + #expect(description == nil) + #expect(defaultValue == nil) + #expect(examples == nil) + #expect(enumValue == nil) + #expect(constValue == nil) + #expect(properties.count == 4) + #expect(required == []) + #expect(additionalProperties == nil) + } } -} -@Test func testArraySchema() throws { - let schema: JSONSchema = .array( - title: "Numbers", - description: "An array of numbers", - default: [1, 2, 3], - examples: [[4, 5, 6], [7, 8, 9]], - enum: [[1, 2], [3, 4]], - const: [9, 8, 7], - items: .number(minimum: 0), - minItems: 1, - maxItems: 10, - uniqueItems: true - ) + @Test func testArraySchema() throws { + let schema: JSONSchema = .array( + title: "Numbers", + description: "An array of numbers", + default: [1, 2, 3], + examples: [[4, 5, 6], [7, 8, 9]], + enum: [[1, 2], [3, 4]], + const: [9, 8, 7], + items: .number(minimum: 0), + minItems: 1, + maxItems: 10, + uniqueItems: true + ) - let encoder = JSONEncoder() - let data = try encoder.encode(schema) + let encoder = JSONEncoder() + let data = try encoder.encode(schema) - let decoder = JSONDecoder() - let decodedSchema = try decoder.decode(JSONSchema.self, from: data) + let decoder = JSONDecoder() + let decodedSchema = try decoder.decode(JSONSchema.self, from: data) - #expect(decodedSchema == schema) + #expect(decodedSchema == schema) - if case let .array( - title, description, defaultValue, examples, enumValue, constValue, items, minItems, - maxItems, uniqueItems) = decodedSchema - { - #expect(title == "Numbers") - #expect(description == "An array of numbers") - #expect(defaultValue?.arrayValue?.count == 3) - #expect(examples?.count == 2) - #expect(enumValue?.count == 2) - #expect(constValue?.arrayValue?.count == 3) + if case let .array( + title, description, defaultValue, examples, enumValue, constValue, items, minItems, + maxItems, uniqueItems) = decodedSchema + { + #expect(title == "Numbers") + #expect(description == "An array of numbers") + #expect(defaultValue?.arrayValue?.count == 3) + #expect(examples?.count == 2) + #expect(enumValue?.count == 2) + #expect(constValue?.arrayValue?.count == 3) + + if case let .number(_, _, _, _, _, _, minimum, _, _, _, _) = items { + #expect(minimum == 0) + } else { + Issue.record("items should be a number schema") + } - if case let .number(_, _, _, _, _, _, minimum, _, _, _, _) = items { - #expect(minimum == 0) + #expect(minItems == 1) + #expect(maxItems == 10) + #expect(uniqueItems == true) } else { - Issue.record("items should be a number schema") + Issue.record("Decoded schema should be an array schema") } - - #expect(minItems == 1) - #expect(maxItems == 10) - #expect(uniqueItems == true) - } else { - Issue.record("Decoded schema should be an array schema") } -} -@Test func testStringSchema() throws { - let schema: JSONSchema = .string( - title: "Email", - description: "A valid email address", - default: "john@example.com", - examples: ["jane@example.com", "support@example.com"], - enum: ["admin@example.com", "user@example.com"], - const: "constant@example.com", - minLength: 5, - maxLength: 100, - pattern: "^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$", - format: .email - ) + @Test func testStringSchema() throws { + let schema: JSONSchema = .string( + title: "Email", + description: "A valid email address", + default: "john@example.com", + examples: ["jane@example.com", "support@example.com"], + enum: ["admin@example.com", "user@example.com"], + const: "constant@example.com", + minLength: 5, + maxLength: 100, + pattern: "^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$", + format: .email + ) - let encoder = JSONEncoder() - let data = try encoder.encode(schema) - - let decoder = JSONDecoder() - let decodedSchema = try decoder.decode(JSONSchema.self, from: data) - - #expect(decodedSchema == schema) - - if case let .string( - title, description, defaultValue, examples, enumValue, constValue, minLength, maxLength, - pattern, format) = decodedSchema - { - #expect(title == "Email") - #expect(description == "A valid email address") - #expect(defaultValue?.stringValue == "john@example.com") - #expect(examples?.count == 2) - #expect(enumValue?.count == 2) - #expect(constValue?.stringValue == "constant@example.com") - #expect(minLength == 5) - #expect(maxLength == 100) - #expect(pattern == "^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$") - #expect(format == .email) - } else { - Issue.record("Decoded schema should be a string schema") - } -} - -@Test func testNumberSchema() throws { - let schema: JSONSchema = .number( - title: "Temperature", - description: "Temperature in Celsius", - default: 20.5, - examples: [18.0, 22.5], - enum: [0.0, 100.0], - const: 37.0, - minimum: -273.15, - maximum: 1000.0, - exclusiveMinimum: -273.15, - exclusiveMaximum: 1000.0, - multipleOf: 0.5 - ) + let encoder = JSONEncoder() + let data = try encoder.encode(schema) - let encoder = JSONEncoder() - let data = try encoder.encode(schema) - - let decoder = JSONDecoder() - let decodedSchema = try decoder.decode(JSONSchema.self, from: data) - - // Don't directly compare whole schemas as floating point representations may differ - // #expect(decodedSchema == schema) - - if case let .number( - title, description, defaultValue, examples, enumValue, constValue, minimum, maximum, - exclusiveMinimum, exclusiveMaximum, multipleOf) = decodedSchema - { - #expect(title == "Temperature") - #expect(description == "Temperature in Celsius") - #expect(defaultValue?.doubleValue == 20.5) - #expect(examples?.count == 2) - #expect(enumValue?.count == 2) - #expect(constValue?.doubleValue == 37.0) - #expect(minimum == -273.15) - #expect(maximum == 1000.0) - #expect(exclusiveMinimum == -273.15) - #expect(exclusiveMaximum == 1000.0) - #expect(multipleOf == 0.5) - } else { - Issue.record("Decoded schema should be a number schema") - } -} + let decoder = JSONDecoder() + let decodedSchema = try decoder.decode(JSONSchema.self, from: data) -@Test func testIntegerSchema() throws { - let schema: JSONSchema = .integer( - title: "Age", - description: "Age in years", - default: 30, - examples: [25, 40], - enum: [18, 21, 65], - const: 42, - minimum: 0, - maximum: 120, - exclusiveMinimum: 0, - exclusiveMaximum: 120, - multipleOf: 1 - ) + #expect(decodedSchema == schema) - let encoder = JSONEncoder() - let data = try encoder.encode(schema) - - let decoder = JSONDecoder() - let decodedSchema = try decoder.decode(JSONSchema.self, from: data) - - #expect(decodedSchema == schema) - - if case let .integer( - title, description, defaultValue, examples, enumValue, constValue, minimum, maximum, - exclusiveMinimum, exclusiveMaximum, multipleOf) = decodedSchema - { - #expect(title == "Age") - #expect(description == "Age in years") - #expect(defaultValue?.intValue == 30) - #expect(examples?.count == 2) - #expect(enumValue?.count == 3) - #expect(constValue?.intValue == 42) - #expect(minimum == 0) - #expect(maximum == 120) - #expect(exclusiveMinimum == 0) - #expect(exclusiveMaximum == 120) - #expect(multipleOf == 1) - } else { - Issue.record("Decoded schema should be an integer schema") + if case let .string( + title, description, defaultValue, examples, enumValue, constValue, minLength, maxLength, + pattern, format) = decodedSchema + { + #expect(title == "Email") + #expect(description == "A valid email address") + #expect(defaultValue?.stringValue == "john@example.com") + #expect(examples?.count == 2) + #expect(enumValue?.count == 2) + #expect(constValue?.stringValue == "constant@example.com") + #expect(minLength == 5) + #expect(maxLength == 100) + #expect(pattern == "^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$") + #expect(format == .email) + } else { + Issue.record("Decoded schema should be a string schema") + } } -} -@Test func testBooleanSchema() throws { - let schema: JSONSchema = .boolean( - title: "Active", - description: "Whether the user is active", - default: true - ) + @Test func testNumberSchema() throws { + let schema: JSONSchema = .number( + title: "Temperature", + description: "Temperature in Celsius", + default: 20.5, + examples: [18.0, 22.5], + enum: [0.0, 100.0], + const: 37.0, + minimum: -273.15, + maximum: 1000.0, + exclusiveMinimum: -273.15, + exclusiveMaximum: 1000.0, + multipleOf: 0.5 + ) - let encoder = JSONEncoder() - let data = try encoder.encode(schema) + let encoder = JSONEncoder() + let data = try encoder.encode(schema) - let decoder = JSONDecoder() - let decodedSchema = try decoder.decode(JSONSchema.self, from: data) + let decoder = JSONDecoder() + let decodedSchema = try decoder.decode(JSONSchema.self, from: data) - #expect(decodedSchema == schema) + // Verify schemas are equivalent using sorted JSON comparison + try assertJSONSchemaEquivalent(decodedSchema, schema) - if case let .boolean(title, description, defaultValue) = decodedSchema { - #expect(title == "Active") - #expect(description == "Whether the user is active") - #expect(defaultValue?.boolValue == true) - } else { - Issue.record("Decoded schema should be a boolean schema") + if case let .number( + title, description, defaultValue, examples, enumValue, constValue, minimum, maximum, + exclusiveMinimum, exclusiveMaximum, multipleOf) = decodedSchema + { + #expect(title == "Temperature") + #expect(description == "Temperature in Celsius") + #expect(defaultValue?.doubleValue == 20.5) + #expect(examples?.count == 2) + #expect(enumValue?.count == 2) + #expect(constValue?.doubleValue == 37.0) + #expect(minimum == -273.15) + #expect(maximum == 1000.0) + #expect(exclusiveMinimum == -273.15) + #expect(exclusiveMaximum == 1000.0) + #expect(multipleOf == 0.5) + } else { + Issue.record("Decoded schema should be a number schema") + } } -} -@Test func testNullSchema() throws { - let schema: JSONSchema = .null + @Test func testIntegerSchema() throws { + let schema: JSONSchema = .integer( + title: "Age", + description: "Age in years", + default: 30, + examples: [25, 40], + enum: [18, 21, 65], + const: 42, + minimum: 0, + maximum: 120, + exclusiveMinimum: 0, + exclusiveMaximum: 120, + multipleOf: 1 + ) - let encoder = JSONEncoder() - let data = try encoder.encode(schema) + let encoder = JSONEncoder() + let data = try encoder.encode(schema) - let decoder = JSONDecoder() - let decodedSchema = try decoder.decode(JSONSchema.self, from: data) + let decoder = JSONDecoder() + let decodedSchema = try decoder.decode(JSONSchema.self, from: data) - #expect(decodedSchema == schema) + #expect(decodedSchema == schema) - if case .null = decodedSchema { - // This is expected - } else { - Issue.record("Decoded schema should be a null schema") + if case let .integer( + title, description, defaultValue, examples, enumValue, constValue, minimum, maximum, + exclusiveMinimum, exclusiveMaximum, multipleOf) = decodedSchema + { + #expect(title == "Age") + #expect(description == "Age in years") + #expect(defaultValue?.intValue == 30) + #expect(examples?.count == 2) + #expect(enumValue?.count == 3) + #expect(constValue?.intValue == 42) + #expect(minimum == 0) + #expect(maximum == 120) + #expect(exclusiveMinimum == 0) + #expect(exclusiveMaximum == 120) + #expect(multipleOf == 1) + } else { + Issue.record("Decoded schema should be an integer schema") + } } -} -@Test func testReferenceSchema() throws { - let schema: JSONSchema = .reference("#/definitions/Person") + @Test func testBooleanSchema() throws { + let schema: JSONSchema = .boolean( + title: "Active", + description: "Whether the user is active", + default: true + ) - let encoder = JSONEncoder() - let data = try encoder.encode(schema) + let encoder = JSONEncoder() + let data = try encoder.encode(schema) - let decoder = JSONDecoder() - let decodedSchema = try decoder.decode(JSONSchema.self, from: data) + let decoder = JSONDecoder() + let decodedSchema = try decoder.decode(JSONSchema.self, from: data) - #expect(decodedSchema == schema) + #expect(decodedSchema == schema) - if case let .reference(ref) = decodedSchema { - #expect(ref == "#/definitions/Person") - } else { - Issue.record("Decoded schema should be a reference schema") + if case let .boolean(title, description, defaultValue) = decodedSchema { + #expect(title == "Active") + #expect(description == "Whether the user is active") + #expect(defaultValue?.boolValue == true) + } else { + Issue.record("Decoded schema should be a boolean schema") + } } -} -@Test func testAnyOfSchema() throws { - let schema: JSONSchema = .anyOf([ - .string(), - .integer(), - .boolean(), - ]) + @Test func testNullSchema() throws { + let schema: JSONSchema = .null - let encoder = JSONEncoder() - let data = try encoder.encode(schema) + let encoder = JSONEncoder() + let data = try encoder.encode(schema) - let decoder = JSONDecoder() - let decodedSchema = try decoder.decode(JSONSchema.self, from: data) + let decoder = JSONDecoder() + let decodedSchema = try decoder.decode(JSONSchema.self, from: data) - #expect(decodedSchema == schema) + #expect(decodedSchema == schema) - if case let .anyOf(schemas) = decodedSchema { - #expect(schemas.count == 3) - } else { - Issue.record("Decoded schema should be an anyOf schema") + if case .null = decodedSchema { + // This is expected + } else { + Issue.record("Decoded schema should be a null schema") + } } -} -@Test func testAllOfSchema() throws { - let schema: JSONSchema = .allOf([ - .object(properties: ["name": .string()]), - .object(properties: ["age": .integer()]), - ]) + @Test func testReferenceSchema() throws { + let schema: JSONSchema = .reference("#/definitions/Person") - let encoder = JSONEncoder() - let data = try encoder.encode(schema) + let encoder = JSONEncoder() + let data = try encoder.encode(schema) - let decoder = JSONDecoder() - let decodedSchema = try decoder.decode(JSONSchema.self, from: data) + let decoder = JSONDecoder() + let decodedSchema = try decoder.decode(JSONSchema.self, from: data) - #expect(decodedSchema == schema) + #expect(decodedSchema == schema) - if case let .allOf(schemas) = decodedSchema { - #expect(schemas.count == 2) - } else { - Issue.record("Decoded schema should be an allOf schema") + if case let .reference(ref) = decodedSchema { + #expect(ref == "#/definitions/Person") + } else { + Issue.record("Decoded schema should be a reference schema") + } } -} -@Test func testOneOfSchema() throws { - let schema: JSONSchema = .oneOf([ - .object(properties: ["type": .string(const: "dog"), "bark": .boolean()]), - .object(properties: ["type": .string(const: "cat"), "meow": .boolean()]), - ]) + @Test func testAnyOfSchema() throws { + let schema: JSONSchema = .anyOf([ + .string(), + .integer(), + .boolean(), + ]) - let encoder = JSONEncoder() - let data = try encoder.encode(schema) + let encoder = JSONEncoder() + let data = try encoder.encode(schema) - let decoder = JSONDecoder() - let decodedSchema = try decoder.decode(JSONSchema.self, from: data) + let decoder = JSONDecoder() + let decodedSchema = try decoder.decode(JSONSchema.self, from: data) - #expect(decodedSchema == schema) + #expect(decodedSchema == schema) - if case let .oneOf(schemas) = decodedSchema { - #expect(schemas.count == 2) - } else { - Issue.record("Decoded schema should be a oneOf schema") + if case let .anyOf(schemas) = decodedSchema { + #expect(schemas.count == 3) + } else { + Issue.record("Decoded schema should be an anyOf schema") + } } -} -@Test func testNotSchema() throws { - let schema: JSONSchema = .not(.string()) + @Test func testAllOfSchema() throws { + let schema: JSONSchema = .allOf([ + .object(properties: ["name": .string()]), + .object(properties: ["age": .integer()]), + ]) - let encoder = JSONEncoder() - let data = try encoder.encode(schema) + let encoder = JSONEncoder() + let data = try encoder.encode(schema) - let decoder = JSONDecoder() - let decodedSchema = try decoder.decode(JSONSchema.self, from: data) + let decoder = JSONDecoder() + let decodedSchema = try decoder.decode(JSONSchema.self, from: data) - #expect(decodedSchema == schema) + // Verify schemas are equivalent using sorted JSON comparison + try assertJSONSchemaEquivalent(decodedSchema, schema) - if case let .not(notSchema) = decodedSchema { - if case .string = notSchema { - // This is expected + if case let .allOf(schemas) = decodedSchema { + #expect(schemas.count == 2) } else { - Issue.record("Inner schema should be a string schema") + Issue.record("Decoded schema should be an allOf schema") } - } else { - Issue.record("Decoded schema should be a not schema") } -} -@Test func testEmptySchema() throws { - let schema: JSONSchema = .empty + @Test func testOneOfSchema() throws { + let schema: JSONSchema = .oneOf([ + .object(properties: ["type": .string(const: "dog"), "bark": .boolean()]), + .object(properties: ["type": .string(const: "cat"), "meow": .boolean()]), + ]) - let encoder = JSONEncoder() - let data = try encoder.encode(schema) + let encoder = JSONEncoder() + let data = try encoder.encode(schema) - let decoder = JSONDecoder() - let decodedSchema = try decoder.decode(JSONSchema.self, from: data) + let decoder = JSONDecoder() + let decodedSchema = try decoder.decode(JSONSchema.self, from: data) - #expect(decodedSchema == schema) + // Verify schemas are equivalent using sorted JSON comparison + try assertJSONSchemaEquivalent(decodedSchema, schema) - if case .empty = decodedSchema { - // This is expected - } else { - Issue.record("Decoded schema should be an empty schema") + if case let .oneOf(schemas) = decodedSchema { + #expect(schemas.count == 2) + } else { + Issue.record("Decoded schema should be a oneOf schema") + } } -} -@Test func testAnySchema() throws { - let schema: JSONSchema = .any + @Test func testNotSchema() throws { + let schema: JSONSchema = .not(.string()) - let encoder = JSONEncoder() - let data = try encoder.encode(schema) + let encoder = JSONEncoder() + let data = try encoder.encode(schema) - let decoder = JSONDecoder() - let decodedSchema = try decoder.decode(JSONSchema.self, from: data) + let decoder = JSONDecoder() + let decodedSchema = try decoder.decode(JSONSchema.self, from: data) - #expect(decodedSchema == schema) + #expect(decodedSchema == schema) - if case .any = decodedSchema { - // This is expected - } else { - Issue.record("Decoded schema should be an any schema") + if case let .not(notSchema) = decodedSchema { + if case .string = notSchema { + // This is expected + } else { + Issue.record("Inner schema should be a string schema") + } + } else { + Issue.record("Decoded schema should be a not schema") + } } -} - -@Test func testStringFormatEncoding() throws { - // Test each format - let formats: [StringFormat] = [ - .dateTime, .date, .time, .duration, .email, .idnEmail, - .hostname, .idnHostname, .ipv4, .ipv6, .uri, .uriReference, - .iriReference, .uriTemplate, .jsonPointer, .relativeJsonPointer, - .regex, .uuid, .custom("custom-format"), - ] - for format in formats { - let schema: JSONSchema = .string(format: format) + @Test func testEmptySchema() throws { + let schema: JSONSchema = .empty let encoder = JSONEncoder() let data = try encoder.encode(schema) @@ -473,670 +454,786 @@ import Testing let decoder = JSONDecoder() let decodedSchema = try decoder.decode(JSONSchema.self, from: data) - if case let .string(_, _, _, _, _, _, _, _, _, decodedFormat) = decodedSchema, - let decodedFormat = decodedFormat - { - #expect(decodedFormat == format) + #expect(decodedSchema == schema) + + if case .empty = decodedSchema { + // This is expected } else { - Issue.record("Format \(format) was not correctly encoded/decoded") + Issue.record("Decoded schema should be an empty schema") } } -} -@Test func testStringFormatRawValue() { - #expect(StringFormat.dateTime.rawValue == "date-time") - #expect(StringFormat.date.rawValue == "date") - #expect(StringFormat.time.rawValue == "time") - #expect(StringFormat.duration.rawValue == "duration") - #expect(StringFormat.email.rawValue == "email") - #expect(StringFormat.idnEmail.rawValue == "idn-email") - #expect(StringFormat.hostname.rawValue == "hostname") - #expect(StringFormat.idnHostname.rawValue == "idn-hostname") - #expect(StringFormat.ipv4.rawValue == "ipv4") - #expect(StringFormat.ipv6.rawValue == "ipv6") - #expect(StringFormat.uri.rawValue == "uri") - #expect(StringFormat.uriReference.rawValue == "uri-reference") - #expect(StringFormat.iriReference.rawValue == "iri-reference") - #expect(StringFormat.uriTemplate.rawValue == "uri-template") - #expect(StringFormat.jsonPointer.rawValue == "json-pointer") - #expect(StringFormat.relativeJsonPointer.rawValue == "relative-json-pointer") - #expect(StringFormat.regex.rawValue == "regex") - #expect(StringFormat.uuid.rawValue == "uuid") - #expect(StringFormat.custom("custom-format").rawValue == "custom-format") -} + @Test func testAnySchema() throws { + let schema: JSONSchema = .any -@Test func testAdditionalPropertiesEncoding() throws { - // Test boolean variant - let schemaWithBooleanAdditionalProps: JSONSchema = .object( - additionalProperties: .boolean(true) - ) + let encoder = JSONEncoder() + let data = try encoder.encode(schema) - let encoder = JSONEncoder() - var data = try encoder.encode(schemaWithBooleanAdditionalProps) + let decoder = JSONDecoder() + let decodedSchema = try decoder.decode(JSONSchema.self, from: data) - let decoder = JSONDecoder() - var decodedSchema = try decoder.decode(JSONSchema.self, from: data) + #expect(decodedSchema == schema) - if case let .object(_, _, _, _, _, _, _, _, additionalProperties) = decodedSchema, - let additionalProperties = additionalProperties - { - if case let .boolean(value) = additionalProperties { - #expect(value == true) + if case .any = decodedSchema { + // This is expected } else { - Issue.record("additionalProperties should be .boolean(true)") + Issue.record("Decoded schema should be an any schema") } - } else { - Issue.record("Decoded schema should be an object with additionalProperties") } - // Test schema variant - let schemaWithSchemaAdditionalProps: JSONSchema = .object( - additionalProperties: .schema(.string(minLength: 1)) - ) + @Test func testStringFormatEncoding() throws { + // Test each format + let formats: [StringFormat] = [ + .dateTime, .date, .time, .duration, .email, .idnEmail, + .hostname, .idnHostname, .ipv4, .ipv6, .uri, .uriReference, + .iriReference, .uriTemplate, .jsonPointer, .relativeJsonPointer, + .regex, .uuid, .custom("custom-format"), + ] - data = try encoder.encode(schemaWithSchemaAdditionalProps) - decodedSchema = try decoder.decode(JSONSchema.self, from: data) + for format in formats { + let schema: JSONSchema = .string(format: format) - if case let .object(_, _, _, _, _, _, _, _, additionalProperties) = decodedSchema, - let additionalProperties = additionalProperties - { - if case let .schema(additionalPropsSchema) = additionalProperties { - if case let .string(_, _, _, _, _, _, minLength, _, _, _) = additionalPropsSchema { - #expect(minLength == 1) + let encoder = JSONEncoder() + let data = try encoder.encode(schema) + + let decoder = JSONDecoder() + let decodedSchema = try decoder.decode(JSONSchema.self, from: data) + + if case let .string(_, _, _, _, _, _, _, _, _, decodedFormat) = decodedSchema, + let decodedFormat = decodedFormat + { + #expect(decodedFormat == format) } else { - Issue.record("Expected string schema") + Issue.record("Format \(format) was not correctly encoded/decoded") } - } else { - Issue.record("Expected schema variant") } - } else { - Issue.record("Decoded schema should be an object with additionalProperties") } -} -@Test func testAdditionalPropertiesBooleanVariant() throws { - let trueVariant: AdditionalProperties = .boolean(true) - let falseVariant: AdditionalProperties = .boolean(false) + @Test func testStringFormatRawValue() { + #expect(StringFormat.dateTime.rawValue == "date-time") + #expect(StringFormat.date.rawValue == "date") + #expect(StringFormat.time.rawValue == "time") + #expect(StringFormat.duration.rawValue == "duration") + #expect(StringFormat.email.rawValue == "email") + #expect(StringFormat.idnEmail.rawValue == "idn-email") + #expect(StringFormat.hostname.rawValue == "hostname") + #expect(StringFormat.idnHostname.rawValue == "idn-hostname") + #expect(StringFormat.ipv4.rawValue == "ipv4") + #expect(StringFormat.ipv6.rawValue == "ipv6") + #expect(StringFormat.uri.rawValue == "uri") + #expect(StringFormat.uriReference.rawValue == "uri-reference") + #expect(StringFormat.iriReference.rawValue == "iri-reference") + #expect(StringFormat.uriTemplate.rawValue == "uri-template") + #expect(StringFormat.jsonPointer.rawValue == "json-pointer") + #expect(StringFormat.relativeJsonPointer.rawValue == "relative-json-pointer") + #expect(StringFormat.regex.rawValue == "regex") + #expect(StringFormat.uuid.rawValue == "uuid") + #expect(StringFormat.custom("custom-format").rawValue == "custom-format") + } - // Test equality - #expect(trueVariant == trueVariant) - #expect(falseVariant == falseVariant) - #expect(trueVariant != falseVariant) + @Test func testAdditionalPropertiesEncoding() throws { + // Test boolean variant + let schemaWithBooleanAdditionalProps: JSONSchema = .object( + additionalProperties: .boolean(true) + ) - // Test encoding and decoding - let encoder = JSONEncoder() + let encoder = JSONEncoder() + var data = try encoder.encode(schemaWithBooleanAdditionalProps) - let trueData = try encoder.encode(trueVariant) - let falseData = try encoder.encode(falseVariant) + let decoder = JSONDecoder() + var decodedSchema = try decoder.decode(JSONSchema.self, from: data) - let decoder = JSONDecoder() - let decodedTrueVariant = try decoder.decode(AdditionalProperties.self, from: trueData) - let decodedFalseVariant = try decoder.decode(AdditionalProperties.self, from: falseData) + if case let .object(_, _, _, _, _, _, _, _, additionalProperties) = decodedSchema, + let additionalProperties = additionalProperties + { + if case let .boolean(value) = additionalProperties { + #expect(value == true) + } else { + Issue.record("additionalProperties should be .boolean(true)") + } + } else { + Issue.record("Decoded schema should be an object with additionalProperties") + } - #expect(decodedTrueVariant == trueVariant) - #expect(decodedFalseVariant == falseVariant) + // Test schema variant + let schemaWithSchemaAdditionalProps: JSONSchema = .object( + additionalProperties: .schema(.string(minLength: 1)) + ) - if case let .boolean(value) = decodedTrueVariant { - #expect(value == true) - } else { - Issue.record("Expected boolean variant with true value") - } + data = try encoder.encode(schemaWithSchemaAdditionalProps) + decodedSchema = try decoder.decode(JSONSchema.self, from: data) - if case let .boolean(value) = decodedFalseVariant { - #expect(value == false) - } else { - Issue.record("Expected boolean variant with false value") + if case let .object(_, _, _, _, _, _, _, _, additionalProperties) = decodedSchema, + let additionalProperties = additionalProperties + { + if case let .schema(additionalPropsSchema) = additionalProperties { + if case let .string(_, _, _, _, _, _, minLength, _, _, _) = additionalPropsSchema { + #expect(minLength == 1) + } else { + Issue.record("Expected string schema") + } + } else { + Issue.record("Expected schema variant") + } + } else { + Issue.record("Decoded schema should be an object with additionalProperties") + } } - // Verify the encoded JSON - let trueJson = String(data: trueData, encoding: .utf8) - let falseJson = String(data: falseData, encoding: .utf8) + @Test func testAdditionalPropertiesBooleanVariant() throws { + let trueVariant: AdditionalProperties = .boolean(true) + let falseVariant: AdditionalProperties = .boolean(false) - #expect(trueJson == "true") - #expect(falseJson == "false") -} + // Test equality + #expect(trueVariant == trueVariant) + #expect(falseVariant == falseVariant) + #expect(trueVariant != falseVariant) -@Test func testAdditionalPropertiesSchemaVariant() throws { - let stringSchema: JSONSchema = .string(minLength: 1, maxLength: 100) - let schemaVariant: AdditionalProperties = .schema(stringSchema) + // Test encoding and decoding + let encoder = JSONEncoder() - // Test equality - #expect(schemaVariant == schemaVariant) - #expect(schemaVariant != .boolean(true)) - #expect(schemaVariant != .schema(.integer())) + let trueData = try encoder.encode(trueVariant) + let falseData = try encoder.encode(falseVariant) - // Test encoding and decoding - let encoder = JSONEncoder() - let data = try encoder.encode(schemaVariant) + let decoder = JSONDecoder() + let decodedTrueVariant = try decoder.decode(AdditionalProperties.self, from: trueData) + let decodedFalseVariant = try decoder.decode(AdditionalProperties.self, from: falseData) - let decoder = JSONDecoder() - let decodedVariant = try decoder.decode(AdditionalProperties.self, from: data) + #expect(decodedTrueVariant == trueVariant) + #expect(decodedFalseVariant == falseVariant) - #expect(decodedVariant == schemaVariant) + if case let .boolean(value) = decodedTrueVariant { + #expect(value == true) + } else { + Issue.record("Expected boolean variant with true value") + } - if case let .schema(decodedSchema) = decodedVariant { - if case let .string(_, _, _, _, _, _, minLength, _, _, _) = decodedSchema { - #expect(minLength == 1) + if case let .boolean(value) = decodedFalseVariant { + #expect(value == false) } else { - Issue.record("Expected string schema") + Issue.record("Expected boolean variant with false value") } - } else { - Issue.record("Expected schema variant") - } -} -@Test func testExpressibleByLiteralProtocols() { - // Dictionary literal for object schema - let objectSchema: JSONSchema = [ - "name": .string(), - "age": .integer(), - ] - - if case let .object(_, _, _, _, _, _, properties, _, _) = objectSchema { - #expect(properties.count == 2) - #expect(properties["name"] != nil) - #expect(properties["age"] != nil) - } else { - Issue.record("Schema should be an object schema") + // Verify the encoded JSON + let trueJson = String(data: trueData, encoding: .utf8) + let falseJson = String(data: falseData, encoding: .utf8) + + #expect(trueJson == "true") + #expect(falseJson == "false") } - // Boolean literal - let trueSchema: JSONSchema = true - let falseSchema: JSONSchema = false + @Test func testAdditionalPropertiesSchemaVariant() throws { + let stringSchema: JSONSchema = .string(minLength: 1, maxLength: 100) + let schemaVariant: AdditionalProperties = .schema(stringSchema) - if case .any = trueSchema { - // This is expected - } else { - Issue.record("trueSchema should be .any") - } + // Test equality + #expect(schemaVariant == schemaVariant) + #expect(schemaVariant != .boolean(true)) + #expect(schemaVariant != .schema(.integer())) + + // Test encoding and decoding + let encoder = JSONEncoder() + let data = try encoder.encode(schemaVariant) + + let decoder = JSONDecoder() + let decodedVariant = try decoder.decode(AdditionalProperties.self, from: data) + + #expect(decodedVariant == schemaVariant) - if case .not(let schema) = falseSchema, case .any = schema { - // This is expected - } else { - Issue.record("falseSchema should be .not(.any)") + if case let .schema(decodedSchema) = decodedVariant { + if case let .string(_, _, _, _, _, _, minLength, _, _, _) = decodedSchema { + #expect(minLength == 1) + } else { + Issue.record("Expected string schema") + } + } else { + Issue.record("Expected schema variant") + } } - // Nil literal - let nilSchema: JSONSchema = nil + @Test func testExpressibleByLiteralProtocols() { + // Dictionary literal for object schema + let objectSchema: JSONSchema = [ + "name": .string(), + "age": .integer(), + ] - if case .empty = nilSchema { - // This is expected - } else { - Issue.record("nilSchema should be .empty") + if case let .object(_, _, _, _, _, _, properties, _, _) = objectSchema { + #expect(properties.count == 2) + #expect(properties["name"] != nil) + #expect(properties["age"] != nil) + } else { + Issue.record("Schema should be an object schema") + } + + // Boolean literal + let trueSchema: JSONSchema = true + let falseSchema: JSONSchema = false + + if case .any = trueSchema { + // This is expected + } else { + Issue.record("trueSchema should be .any") + } + + if case .not(let schema) = falseSchema, case .any = schema { + // This is expected + } else { + Issue.record("falseSchema should be .not(.any)") + } + + // Nil literal + let nilSchema: JSONSchema = nil + + if case .empty = nilSchema { + // This is expected + } else { + Issue.record("nilSchema should be .empty") + } } -} -@Test func testComplexSchema() throws { - // Create a complex schema with nested objects, arrays, and constraints - let schema: JSONSchema = .object( - title: "Person", - description: "A person schema with various fields", - properties: [ - "name": .string(minLength: 2, maxLength: 100), - "age": .integer(minimum: 0, maximum: 120), - "email": .string(format: .email), - "address": .object( - properties: [ - "street": .string(), - "city": .string(), - "state": .string(minLength: 2, maxLength: 2), - "zip": .string(pattern: "^[0-9]{5}$"), - "country": .string(), - ], - required: ["street", "city", "country"] - ), - "phoneNumbers": .array( - items: .object( + @Test func testComplexSchema() throws { + // Create a complex schema with nested objects, arrays, and constraints + let schema: JSONSchema = .object( + title: "Person", + description: "A person schema with various fields", + properties: [ + "name": .string(minLength: 2, maxLength: 100), + "age": .integer(minimum: 0, maximum: 120), + "email": .string(format: .email), + "address": .object( properties: [ - "type": .string(enum: ["home", "work", "mobile"]), - "number": .string(pattern: "^[0-9-+()\\s]+$"), + "street": .string(), + "city": .string(), + "state": .string(minLength: 2, maxLength: 2), + "zip": .string(pattern: "^[0-9]{5}$"), + "country": .string(), ], - required: ["type", "number"] + required: ["street", "city", "country"] ), - minItems: 1 - ), - "status": .oneOf([ - .string(enum: ["active", "inactive", "pending"]), - .object( - properties: [ - "code": .integer(), - "message": .string(), - ], - required: ["code", "message"] + "phoneNumbers": .array( + items: .object( + properties: [ + "type": .string(enum: ["home", "work", "mobile"]), + "number": .string(pattern: "^[0-9-+()\\s]+$"), + ], + required: ["type", "number"] + ), + minItems: 1 ), - ]), - "metadata": .object(additionalProperties: .boolean(true)), - ], - required: ["name", "email"] - ) + "status": .oneOf([ + .string(enum: ["active", "inactive", "pending"]), + .object( + properties: [ + "code": .integer(), + "message": .string(), + ], + required: ["code", "message"] + ), + ]), + "metadata": .object(additionalProperties: .boolean(true)), + ], + required: ["name", "email"] + ) - let encoder = JSONEncoder() - encoder.outputFormatting = [.prettyPrinted, .sortedKeys] - let data = try encoder.encode(schema) + let encoder = JSONEncoder() + encoder.outputFormatting = [.prettyPrinted] + let data = try encoder.encode(schema) - let decoder = JSONDecoder() - let decodedSchema = try decoder.decode(JSONSchema.self, from: data) + let decoder = JSONDecoder() + let decodedSchema = try decoder.decode(JSONSchema.self, from: data) - #expect(decodedSchema == schema) + // Verify schemas are equivalent using sorted JSON comparison + try assertJSONSchemaEquivalent(decodedSchema, schema) - // Perform more detailed checks on the complex structure - if case let .object(_, _, _, _, _, _, properties, required, _) = decodedSchema { - #expect(properties.count == 7) - #expect(required.contains("name")) - #expect(required.contains("email")) + // Perform more detailed checks on the complex structure + if case let .object(_, _, _, _, _, _, properties, required, _) = decodedSchema { + #expect(properties.count == 7) + #expect(required.contains("name")) + #expect(required.contains("email")) - // Check nested address object - if case let .object(_, _, _, _, _, _, addressProperties, addressRequired, _) = properties[ - "address"] - { - #expect(addressProperties.count == 5) - #expect(addressRequired.contains("street")) - #expect(addressRequired.contains("city")) - #expect(addressRequired.contains("country")) - } else { - Issue.record("address should be an object schema") - } - - // Check phoneNumbers array - if case let .array(_, _, _, _, _, _, phoneItemsSchema, minItems, _, _) = properties[ - "phoneNumbers"] - { - #expect(minItems == 1) + // Check nested address object + if case let .object(_, _, _, _, _, _, addressProperties, addressRequired, _) = + properties[ + "address"] + { + #expect(addressProperties.count == 5) + #expect(addressRequired.contains("street")) + #expect(addressRequired.contains("city")) + #expect(addressRequired.contains("country")) + } else { + Issue.record("address should be an object schema") + } - if case let .object(_, _, _, _, _, _, phoneProperties, phoneRequired, _) = - phoneItemsSchema + // Check phoneNumbers array + if case let .array(_, _, _, _, _, _, phoneItemsSchema, minItems, _, _) = properties[ + "phoneNumbers"] { - #expect(phoneProperties.count == 2) - #expect(phoneRequired.contains("type")) - #expect(phoneRequired.contains("number")) + #expect(minItems == 1) - if case let .string(_, _, _, _, enumValues, _, _, _, _, _) = phoneProperties["type"] + if case let .object(_, _, _, _, _, _, phoneProperties, phoneRequired, _) = + phoneItemsSchema { - #expect(enumValues?.count == 3) - if let enumValues = enumValues { - #expect(enumValues.contains("home")) - #expect(enumValues.contains("work")) - #expect(enumValues.contains("mobile")) + #expect(phoneProperties.count == 2) + #expect(phoneRequired.contains("type")) + #expect(phoneRequired.contains("number")) + + if case let .string(_, _, _, _, enumValues, _, _, _, _, _) = phoneProperties[ + "type"] + { + #expect(enumValues?.count == 3) + if let enumValues = enumValues { + #expect(enumValues.contains("home")) + #expect(enumValues.contains("work")) + #expect(enumValues.contains("mobile")) + } + } else { + Issue.record("type should be a string schema with enum values") } } else { - Issue.record("type should be a string schema with enum values") + Issue.record("phoneNumbers items should be an object schema") } } else { - Issue.record("phoneNumbers items should be an object schema") + Issue.record("phoneNumbers should be an array schema") } - } else { - Issue.record("phoneNumbers should be an array schema") - } - // Check oneOf in status - if case let .oneOf(statusSchemas) = properties["status"] { - #expect(statusSchemas.count == 2) + // Check oneOf in status + if case let .oneOf(statusSchemas) = properties["status"] { + #expect(statusSchemas.count == 2) - if case let .string(_, _, _, _, enumValues, _, _, _, _, _) = statusSchemas[0] { - #expect(enumValues?.count == 3) - if let enumValues = enumValues { - #expect(enumValues.contains("active")) - #expect(enumValues.contains("inactive")) - #expect(enumValues.contains("pending")) + if case let .string(_, _, _, _, enumValues, _, _, _, _, _) = statusSchemas[0] { + #expect(enumValues?.count == 3) + if let enumValues = enumValues { + #expect(enumValues.contains("active")) + #expect(enumValues.contains("inactive")) + #expect(enumValues.contains("pending")) + } + } else { + Issue.record("First status schema should be a string schema with enum values") } - } else { - Issue.record("First status schema should be a string schema with enum values") - } - if case let .object(_, _, _, _, _, _, statusObjProperties, statusObjRequired, _) = - statusSchemas[1] - { - #expect(statusObjProperties.count == 2) - #expect(statusObjRequired.contains("code")) - #expect(statusObjRequired.contains("message")) + if case let .object(_, _, _, _, _, _, statusObjProperties, statusObjRequired, _) = + statusSchemas[1] + { + #expect(statusObjProperties.count == 2) + #expect(statusObjRequired.contains("code")) + #expect(statusObjRequired.contains("message")) + } else { + Issue.record("Second status schema should be an object schema") + } } else { - Issue.record("Second status schema should be an object schema") + Issue.record("status should be a oneOf schema") } } else { - Issue.record("status should be a oneOf schema") + Issue.record("Decoded schema should be an object schema") } - } else { - Issue.record("Decoded schema should be an object schema") } -} -@Test func testJSONValueConversions() throws { - // Test creating JSONValues and converting between types + @Test func testJSONValueConversions() throws { + // Test creating JSONValues and converting between types - // Test null - let nullValue: JSONValue = .null - #expect(nullValue.isNull) + // Test null + let nullValue: JSONValue = .null + #expect(nullValue.isNull) - // Test boolean - let boolValue: JSONValue = true - #expect(boolValue.boolValue == true) + // Test boolean + let boolValue: JSONValue = true + #expect(boolValue.boolValue == true) - // Test integer - let intValue: JSONValue = 42 - #expect(intValue.intValue == 42) + // Test integer + let intValue: JSONValue = 42 + #expect(intValue.intValue == 42) - // Test double - let doubleValue: JSONValue = 3.14 - #expect(doubleValue.doubleValue == 3.14) + // Test double + let doubleValue: JSONValue = 3.14 + #expect(doubleValue.doubleValue == 3.14) - // Test string - let stringValue: JSONValue = "hello" - #expect(stringValue.stringValue == "hello") + // Test string + let stringValue: JSONValue = "hello" + #expect(stringValue.stringValue == "hello") - // Test array - let arrayValue: JSONValue = [1, "string", true] - #expect(arrayValue.arrayValue?.count == 3) + // Test array + let arrayValue: JSONValue = [1, "string", true] + #expect(arrayValue.arrayValue?.count == 3) - // Test object - let objectValue: JSONValue = ["key1": "value1", "key2": 42] - #expect(objectValue.objectValue?.count == 2) - #expect(objectValue.objectValue?["key1"]?.stringValue == "value1") - #expect(objectValue.objectValue?["key2"]?.intValue == 42) + // Test object + let objectValue: JSONValue = ["key1": "value1", "key2": 42] + #expect(objectValue.objectValue?.count == 2) + #expect(objectValue.objectValue?["key1"]?.stringValue == "value1") + #expect(objectValue.objectValue?["key2"]?.intValue == 42) - // Test encoding and decoding - let encoder = JSONEncoder() - let data = try encoder.encode(objectValue) + // Test encoding and decoding + let encoder = JSONEncoder() + let data = try encoder.encode(objectValue) - let decoder = JSONDecoder() - let decodedValue = try decoder.decode(JSONValue.self, from: data) + let decoder = JSONDecoder() + let decodedValue = try decoder.decode(JSONValue.self, from: data) - #expect(decodedValue == objectValue) -} + #expect(decodedValue == objectValue) + } -@Test func testConvenienceProperties() { - // Test object schema - let objectSchema: JSONSchema = .object( - title: "Person", - description: "A person object", - default: ["name": "John"], - examples: [["name": "Jane"]], - enum: [["name": "Option 1"]], - const: ["name": "Constant"] - ) - - #expect(objectSchema.title == "Person") - #expect(objectSchema.description == "A person object") - #expect(objectSchema.default?.objectValue?["name"]?.stringValue == "John") - #expect(objectSchema.examples?.count == 1) - #expect(objectSchema.examples?.first?.objectValue?["name"]?.stringValue == "Jane") - #expect(objectSchema.enum?.count == 1) - #expect(objectSchema.enum?.first?.objectValue?["name"]?.stringValue == "Option 1") - #expect(objectSchema.const?.objectValue?["name"]?.stringValue == "Constant") - - // Test array schema - let arraySchema: JSONSchema = .array( - title: "Numbers", - description: "An array of numbers", - default: [1, 2, 3], - examples: [[4, 5, 6]], - enum: [[7, 8, 9]], - const: [10, 11, 12] - ) - - #expect(arraySchema.title == "Numbers") - #expect(arraySchema.description == "An array of numbers") - #expect(arraySchema.default?.arrayValue?.count == 3) - #expect(arraySchema.examples?.count == 1) - #expect(arraySchema.examples?.first?.arrayValue?.count == 3) - #expect(arraySchema.enum?.count == 1) - #expect(arraySchema.enum?.first?.arrayValue?.count == 3) - #expect(arraySchema.const?.arrayValue?.count == 3) - - // Test string schema - let stringSchema: JSONSchema = .string( - title: "Email", - description: "An email address", - default: "john@example.com", - examples: ["jane@example.com"], - enum: ["admin@example.com"], - const: "constant@example.com" - ) - - #expect(stringSchema.title == "Email") - #expect(stringSchema.description == "An email address") - #expect(stringSchema.default?.stringValue == "john@example.com") - #expect(stringSchema.examples?.count == 1) - #expect(stringSchema.examples?.first?.stringValue == "jane@example.com") - #expect(stringSchema.enum?.count == 1) - #expect(stringSchema.enum?.first?.stringValue == "admin@example.com") - #expect(stringSchema.const?.stringValue == "constant@example.com") - - // Test number schema - let numberSchema: JSONSchema = .number( - title: "Temperature", - description: "Temperature in Celsius", - default: 20.5, - examples: [18.0], - enum: [0.0], - const: 37.0 - ) - - #expect(numberSchema.title == "Temperature") - #expect(numberSchema.description == "Temperature in Celsius") - #expect(numberSchema.default?.doubleValue == 20.5) - #expect(numberSchema.examples?.count == 1) - #expect(numberSchema.examples?.first?.doubleValue == 18.0) - #expect(numberSchema.enum?.count == 1) - #expect(numberSchema.enum?.first?.doubleValue == 0.0) - #expect(numberSchema.const?.doubleValue == 37.0) - - // Test integer schema - let integerSchema: JSONSchema = .integer( - title: "Age", - description: "Age in years", - default: 30, - examples: [25], - enum: [18], - const: 42 - ) - - #expect(integerSchema.title == "Age") - #expect(integerSchema.description == "Age in years") - #expect(integerSchema.default?.intValue == 30) - #expect(integerSchema.examples?.count == 1) - #expect(integerSchema.examples?.first?.intValue == 25) - #expect(integerSchema.enum?.count == 1) - #expect(integerSchema.enum?.first?.intValue == 18) - #expect(integerSchema.const?.intValue == 42) - - // Test boolean schema - let booleanSchema: JSONSchema = .boolean( - title: "Active", - description: "Whether the user is active", - default: true - ) - - #expect(booleanSchema.title == "Active") - #expect(booleanSchema.description == "Whether the user is active") - #expect(booleanSchema.default?.boolValue == true) - - // Test schemas without metadata - let emptyObjectSchema: JSONSchema = .object() - #expect(emptyObjectSchema.title == nil) - #expect(emptyObjectSchema.description == nil) - #expect(emptyObjectSchema.default == nil) - #expect(emptyObjectSchema.examples == nil) - #expect(emptyObjectSchema.enum == nil) - #expect(emptyObjectSchema.const == nil) - - // Test special schemas - let nullSchema: JSONSchema = .null - #expect(nullSchema.title == nil) - #expect(nullSchema.description == nil) - #expect(nullSchema.default == nil) - #expect(nullSchema.examples == nil) - #expect(nullSchema.enum == nil) - #expect(nullSchema.const == nil) - - let anySchema: JSONSchema = .any - #expect(anySchema.title == nil) - #expect(anySchema.description == nil) - #expect(anySchema.default == nil) - #expect(anySchema.examples == nil) - #expect(anySchema.enum == nil) - #expect(anySchema.const == nil) - - let emptySchema: JSONSchema = .empty - #expect(emptySchema.title == nil) - #expect(emptySchema.description == nil) - #expect(emptySchema.default == nil) - #expect(emptySchema.examples == nil) - #expect(emptySchema.enum == nil) - #expect(emptySchema.const == nil) - - let referenceSchema: JSONSchema = .reference("#/definitions/Person") - #expect(referenceSchema.title == nil) - #expect(referenceSchema.description == nil) - #expect(referenceSchema.default == nil) - #expect(referenceSchema.examples == nil) - #expect(referenceSchema.enum == nil) - #expect(referenceSchema.const == nil) - - let anyOfSchema: JSONSchema = .anyOf([.string(), .integer()]) - #expect(anyOfSchema.title == nil) - #expect(anyOfSchema.description == nil) - #expect(anyOfSchema.default == nil) - #expect(anyOfSchema.examples == nil) - #expect(anyOfSchema.enum == nil) - #expect(anyOfSchema.const == nil) - - let allOfSchema: JSONSchema = .allOf([.string(), .integer()]) - #expect(allOfSchema.title == nil) - #expect(allOfSchema.description == nil) - #expect(allOfSchema.default == nil) - #expect(allOfSchema.examples == nil) - #expect(allOfSchema.enum == nil) - #expect(allOfSchema.const == nil) - - let oneOfSchema: JSONSchema = .oneOf([.string(), .integer()]) - #expect(oneOfSchema.title == nil) - #expect(oneOfSchema.description == nil) - #expect(oneOfSchema.default == nil) - #expect(oneOfSchema.examples == nil) - #expect(oneOfSchema.enum == nil) - #expect(oneOfSchema.const == nil) - - let notSchema: JSONSchema = .not(.string()) - #expect(notSchema.title == nil) - #expect(notSchema.description == nil) - #expect(notSchema.default == nil) - #expect(notSchema.examples == nil) - #expect(notSchema.enum == nil) - #expect(notSchema.const == nil) -} + @Test func testConvenienceProperties() { + // Test object schema + let objectSchema: JSONSchema = .object( + title: "Person", + description: "A person object", + default: ["name": "John"], + examples: [["name": "Jane"]], + enum: [["name": "Option 1"]], + const: ["name": "Constant"] + ) + + #expect(objectSchema.title == "Person") + #expect(objectSchema.description == "A person object") + #expect(objectSchema.default?.objectValue?["name"]?.stringValue == "John") + #expect(objectSchema.examples?.count == 1) + #expect(objectSchema.examples?.first?.objectValue?["name"]?.stringValue == "Jane") + #expect(objectSchema.enum?.count == 1) + #expect(objectSchema.enum?.first?.objectValue?["name"]?.stringValue == "Option 1") + #expect(objectSchema.const?.objectValue?["name"]?.stringValue == "Constant") + + // Test array schema + let arraySchema: JSONSchema = .array( + title: "Numbers", + description: "An array of numbers", + default: [1, 2, 3], + examples: [[4, 5, 6]], + enum: [[7, 8, 9]], + const: [10, 11, 12] + ) + + #expect(arraySchema.title == "Numbers") + #expect(arraySchema.description == "An array of numbers") + #expect(arraySchema.default?.arrayValue?.count == 3) + #expect(arraySchema.examples?.count == 1) + #expect(arraySchema.examples?.first?.arrayValue?.count == 3) + #expect(arraySchema.enum?.count == 1) + #expect(arraySchema.enum?.first?.arrayValue?.count == 3) + #expect(arraySchema.const?.arrayValue?.count == 3) + + // Test string schema + let stringSchema: JSONSchema = .string( + title: "Email", + description: "An email address", + default: "john@example.com", + examples: ["jane@example.com"], + enum: ["admin@example.com"], + const: "constant@example.com" + ) + + #expect(stringSchema.title == "Email") + #expect(stringSchema.description == "An email address") + #expect(stringSchema.default?.stringValue == "john@example.com") + #expect(stringSchema.examples?.count == 1) + #expect(stringSchema.examples?.first?.stringValue == "jane@example.com") + #expect(stringSchema.enum?.count == 1) + #expect(stringSchema.enum?.first?.stringValue == "admin@example.com") + #expect(stringSchema.const?.stringValue == "constant@example.com") + + // Test number schema + let numberSchema: JSONSchema = .number( + title: "Temperature", + description: "Temperature in Celsius", + default: 20.5, + examples: [18.0], + enum: [0.0], + const: 37.0 + ) + + #expect(numberSchema.title == "Temperature") + #expect(numberSchema.description == "Temperature in Celsius") + #expect(numberSchema.default?.doubleValue == 20.5) + #expect(numberSchema.examples?.count == 1) + #expect(numberSchema.examples?.first?.doubleValue == 18.0) + #expect(numberSchema.enum?.count == 1) + #expect(numberSchema.enum?.first?.doubleValue == 0.0) + #expect(numberSchema.const?.doubleValue == 37.0) + + // Test integer schema + let integerSchema: JSONSchema = .integer( + title: "Age", + description: "Age in years", + default: 30, + examples: [25], + enum: [18], + const: 42 + ) + + #expect(integerSchema.title == "Age") + #expect(integerSchema.description == "Age in years") + #expect(integerSchema.default?.intValue == 30) + #expect(integerSchema.examples?.count == 1) + #expect(integerSchema.examples?.first?.intValue == 25) + #expect(integerSchema.enum?.count == 1) + #expect(integerSchema.enum?.first?.intValue == 18) + #expect(integerSchema.const?.intValue == 42) + + // Test boolean schema + let booleanSchema: JSONSchema = .boolean( + title: "Active", + description: "Whether the user is active", + default: true + ) + + #expect(booleanSchema.title == "Active") + #expect(booleanSchema.description == "Whether the user is active") + #expect(booleanSchema.default?.boolValue == true) + + // Test schemas without metadata + let emptyObjectSchema: JSONSchema = .object() + #expect(emptyObjectSchema.title == nil) + #expect(emptyObjectSchema.description == nil) + #expect(emptyObjectSchema.default == nil) + #expect(emptyObjectSchema.examples == nil) + #expect(emptyObjectSchema.enum == nil) + #expect(emptyObjectSchema.const == nil) + + // Test special schemas + let nullSchema: JSONSchema = .null + #expect(nullSchema.title == nil) + #expect(nullSchema.description == nil) + #expect(nullSchema.default == nil) + #expect(nullSchema.examples == nil) + #expect(nullSchema.enum == nil) + #expect(nullSchema.const == nil) + + let anySchema: JSONSchema = .any + #expect(anySchema.title == nil) + #expect(anySchema.description == nil) + #expect(anySchema.default == nil) + #expect(anySchema.examples == nil) + #expect(anySchema.enum == nil) + #expect(anySchema.const == nil) + + let emptySchema: JSONSchema = .empty + #expect(emptySchema.title == nil) + #expect(emptySchema.description == nil) + #expect(emptySchema.default == nil) + #expect(emptySchema.examples == nil) + #expect(emptySchema.enum == nil) + #expect(emptySchema.const == nil) + + let referenceSchema: JSONSchema = .reference("#/definitions/Person") + #expect(referenceSchema.title == nil) + #expect(referenceSchema.description == nil) + #expect(referenceSchema.default == nil) + #expect(referenceSchema.examples == nil) + #expect(referenceSchema.enum == nil) + #expect(referenceSchema.const == nil) + + let anyOfSchema: JSONSchema = .anyOf([.string(), .integer()]) + #expect(anyOfSchema.title == nil) + #expect(anyOfSchema.description == nil) + #expect(anyOfSchema.default == nil) + #expect(anyOfSchema.examples == nil) + #expect(anyOfSchema.enum == nil) + #expect(anyOfSchema.const == nil) + + let allOfSchema: JSONSchema = .allOf([.string(), .integer()]) + #expect(allOfSchema.title == nil) + #expect(allOfSchema.description == nil) + #expect(allOfSchema.default == nil) + #expect(allOfSchema.examples == nil) + #expect(allOfSchema.enum == nil) + #expect(allOfSchema.const == nil) + + let oneOfSchema: JSONSchema = .oneOf([.string(), .integer()]) + #expect(oneOfSchema.title == nil) + #expect(oneOfSchema.description == nil) + #expect(oneOfSchema.default == nil) + #expect(oneOfSchema.examples == nil) + #expect(oneOfSchema.enum == nil) + #expect(oneOfSchema.const == nil) + + let notSchema: JSONSchema = .not(.string()) + #expect(notSchema.title == nil) + #expect(notSchema.description == nil) + #expect(notSchema.default == nil) + #expect(notSchema.examples == nil) + #expect(notSchema.enum == nil) + #expect(notSchema.const == nil) + } -@Test func testTypeName() { - // Test object schema - let objectSchema: JSONSchema = .object() - #expect(objectSchema.typeName == "object") + @Test func testTypeName() { + // Test object schema + let objectSchema: JSONSchema = .object() + #expect(objectSchema.typeName == "object") - // Test array schema - let arraySchema: JSONSchema = .array() - #expect(arraySchema.typeName == "array") + // Test array schema + let arraySchema: JSONSchema = .array() + #expect(arraySchema.typeName == "array") - // Test string schema - let stringSchema: JSONSchema = .string() - #expect(stringSchema.typeName == "string") + // Test string schema + let stringSchema: JSONSchema = .string() + #expect(stringSchema.typeName == "string") - // Test number schema - let numberSchema: JSONSchema = .number() - #expect(numberSchema.typeName == "number") + // Test number schema + let numberSchema: JSONSchema = .number() + #expect(numberSchema.typeName == "number") - // Test integer schema - let integerSchema: JSONSchema = .integer() - #expect(integerSchema.typeName == "integer") + // Test integer schema + let integerSchema: JSONSchema = .integer() + #expect(integerSchema.typeName == "integer") - // Test boolean schema - let booleanSchema: JSONSchema = .boolean() - #expect(booleanSchema.typeName == "boolean") + // Test boolean schema + let booleanSchema: JSONSchema = .boolean() + #expect(booleanSchema.typeName == "boolean") - // Test null schema - let nullSchema: JSONSchema = .null - #expect(nullSchema.typeName == "null") + // Test null schema + let nullSchema: JSONSchema = .null + #expect(nullSchema.typeName == "null") - // Test reference schema - let referenceSchema: JSONSchema = .reference("#/definitions/Person") - #expect(referenceSchema.typeName == "reference") + // Test reference schema + let referenceSchema: JSONSchema = .reference("#/definitions/Person") + #expect(referenceSchema.typeName == "reference") - // Test anyOf schema - let anyOfSchema: JSONSchema = .anyOf([.string(), .integer()]) - #expect(anyOfSchema.typeName == "anyOf") + // Test anyOf schema + let anyOfSchema: JSONSchema = .anyOf([.string(), .integer()]) + #expect(anyOfSchema.typeName == "anyOf") - // Test allOf schema - let allOfSchema: JSONSchema = .allOf([.string(), .integer()]) - #expect(allOfSchema.typeName == "allOf") + // Test allOf schema + let allOfSchema: JSONSchema = .allOf([.string(), .integer()]) + #expect(allOfSchema.typeName == "allOf") - // Test oneOf schema - let oneOfSchema: JSONSchema = .oneOf([.string(), .integer()]) - #expect(oneOfSchema.typeName == "oneOf") + // Test oneOf schema + let oneOfSchema: JSONSchema = .oneOf([.string(), .integer()]) + #expect(oneOfSchema.typeName == "oneOf") - // Test not schema - let notSchema: JSONSchema = .not(.string()) - #expect(notSchema.typeName == "not") + // Test not schema + let notSchema: JSONSchema = .not(.string()) + #expect(notSchema.typeName == "not") - // Test empty schema - let emptySchema: JSONSchema = .empty - #expect(emptySchema.typeName == "empty") + // Test empty schema + let emptySchema: JSONSchema = .empty + #expect(emptySchema.typeName == "empty") - // Test any schema - let anySchema: JSONSchema = .any - #expect(anySchema.typeName == "any") -} + // Test any schema + let anySchema: JSONSchema = .any + #expect(anySchema.typeName == "any") + } -@Test func testTypeDefault() { - // Test object schema - let objectSchema: JSONSchema = .object() - #expect(objectSchema.typeDefault?.objectValue?.isEmpty == true) + @Test func testTypeDefault() { + // Test object schema + let objectSchema: JSONSchema = .object() + #expect(objectSchema.typeDefault?.objectValue?.isEmpty == true) - // Test array schema - let arraySchema: JSONSchema = .array() - #expect(arraySchema.typeDefault?.arrayValue?.isEmpty == true) + // Test array schema + let arraySchema: JSONSchema = .array() + #expect(arraySchema.typeDefault?.arrayValue?.isEmpty == true) - // Test string schema - let stringSchema: JSONSchema = .string() - #expect(stringSchema.typeDefault?.stringValue == "") + // Test string schema + let stringSchema: JSONSchema = .string() + #expect(stringSchema.typeDefault?.stringValue == "") - // Test number schema - let numberSchema: JSONSchema = .number() - #expect(numberSchema.typeDefault?.doubleValue == 0.0) + // Test number schema + let numberSchema: JSONSchema = .number() + #expect(numberSchema.typeDefault?.doubleValue == 0.0) - // Test integer schema - let integerSchema: JSONSchema = .integer() - #expect(integerSchema.typeDefault?.intValue == 0) + // Test integer schema + let integerSchema: JSONSchema = .integer() + #expect(integerSchema.typeDefault?.intValue == 0) - // Test boolean schema - let booleanSchema: JSONSchema = .boolean() - #expect(booleanSchema.typeDefault?.boolValue == false) + // Test boolean schema + let booleanSchema: JSONSchema = .boolean() + #expect(booleanSchema.typeDefault?.boolValue == false) - // Test null schema - let nullSchema: JSONSchema = .null - #expect(nullSchema.typeDefault?.isNull == true) + // Test null schema + let nullSchema: JSONSchema = .null + #expect(nullSchema.typeDefault?.isNull == true) - // Test reference schema - let referenceSchema: JSONSchema = .reference("#/definitions/Person") - #expect(referenceSchema.typeDefault == nil) + // Test reference schema + let referenceSchema: JSONSchema = .reference("#/definitions/Person") + #expect(referenceSchema.typeDefault == nil) - // Test anyOf schema - let anyOfSchema: JSONSchema = .anyOf([.string(), .integer()]) - #expect(anyOfSchema.typeDefault == nil) + // Test anyOf schema + let anyOfSchema: JSONSchema = .anyOf([.string(), .integer()]) + #expect(anyOfSchema.typeDefault == nil) - // Test allOf schema - let allOfSchema: JSONSchema = .allOf([.string(), .integer()]) - #expect(allOfSchema.typeDefault == nil) + // Test allOf schema + let allOfSchema: JSONSchema = .allOf([.string(), .integer()]) + #expect(allOfSchema.typeDefault == nil) - // Test oneOf schema - let oneOfSchema: JSONSchema = .oneOf([.string(), .integer()]) - #expect(oneOfSchema.typeDefault == nil) + // Test oneOf schema + let oneOfSchema: JSONSchema = .oneOf([.string(), .integer()]) + #expect(oneOfSchema.typeDefault == nil) - // Test not schema - let notSchema: JSONSchema = .not(.string()) - #expect(notSchema.typeDefault == nil) + // Test not schema + let notSchema: JSONSchema = .not(.string()) + #expect(notSchema.typeDefault == nil) - // Test empty schema - let emptySchema: JSONSchema = .empty - #expect(emptySchema.typeDefault == nil) + // Test empty schema + let emptySchema: JSONSchema = .empty + #expect(emptySchema.typeDefault == nil) - // Test any schema - let anySchema: JSONSchema = .any - #expect(anySchema.typeDefault == nil) + // Test any schema + let anySchema: JSONSchema = .any + #expect(anySchema.typeDefault == nil) + } + + @Test func testPropertiesPreservation() throws { + // Create a schema with properties that don't follow lexicographic ordering + let schema: JSONSchema = .object( + title: "Test Object", + description: "Object with non-lexicographic property names", + properties: [ + "one": .string(minLength: 1), + "two": .integer(minimum: 2), + "three": .boolean(), + "four": .array(items: .number()), + "alpha": .string(), + "zero": .null, + "beta": .object(properties: ["nested": .string()]), + ], + required: ["one", "three"] + ) + + if case let .object(_, _, _, _, _, _, properties, _, _) = schema { + // Check that keys are in the order we inserted them + let orderedKeys = Array(properties.keys) + #expect(orderedKeys == ["one", "two", "three", "four", "alpha", "zero", "beta"]) + } else { + Issue.record("Schema should be an object schema") + } + + let encoder = JSONEncoder() + encoder.outputFormatting = [.prettyPrinted] + let data = try encoder.encode(schema) + + let decoder = JSONDecoder() + let decodedSchema = try decoder.decode(JSONSchema.self, from: data) + + // Verify schemas are equivalent using sorted JSON comparison + try assertJSONSchemaEquivalent(decodedSchema, schema) + + // Verify properties are preserved + if case let .object(title, description, _, _, _, _, properties, _, _) = decodedSchema { + #expect(title == "Test Object") + #expect(description == "Object with non-lexicographic property names") + + // Verify all properties are present + #expect(properties.count == 7) + #expect( + Array(properties.keys) == ["one", "two", "three", "four", "alpha", "zero", "beta"]) + } else { + Issue.record("Decoded schema should be an object schema") + } + + // Also test with dictionary literal syntax + let literalSchema: JSONSchema = [ + "one": .string(), + "two": .integer(), + "three": .boolean(), + "four": .array(), + ] + + let literalData = try encoder.encode(literalSchema) + let decodedLiteralSchema = try decoder.decode(JSONSchema.self, from: literalData) + + if case let .object(_, _, _, _, _, _, properties, _, _) = decodedLiteralSchema { + #expect(properties.count == 4) + #expect(Array(properties.keys) == ["one", "two", "three", "four"]) + } else { + Issue.record("Decoded literal schema should be an object schema") + } + } }