Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 55 additions & 0 deletions Sources/JSONSchema/JSONValue.swift
Original file line number Diff line number Diff line change
Expand Up @@ -422,3 +422,58 @@ extension String {
}
}
}

// MARK: - Schema Compatibility

extension JSONValue {
/// Checks if the basic type of this JSON value matches the type expected by a JSON schema.
///
/// This method primarily checks for type alignment (e.g., JSON object vs. schema object type).
/// It does not perform deep validation of constraints like `minimum`, `maxLength`, etc.
/// For composite schemas (`anyOf`, `allOf`, `oneOf`, `not`), references, `any`, or `empty`,
/// this method generally returns `true`, as compatibility depends on further evaluation.
///
/// - Parameters:
/// - schema: The `JSONSchema` to check compatibility against.
/// - strict: If `true` (default), requires exact numeric type matching (`.int` for `.integer`,
/// `.double` for `.number`). If `false`, allows `.int` to be compatible with `.number`.
/// - Returns: `true` if the value's basic type is compatible with the schema's expected type, `false` otherwise.
public func isCompatible(with schema: JSONSchema, strict: Bool = true) -> Bool {
switch schema {
case .object:
// Check if the JSONValue is an object
guard case .object = self else { return false }
return true
case .array:
// Check if the JSONValue is an array
guard case .array = self else { return false }
return true
case .string:
// Check if the JSONValue is a string
guard case .string = self else { return false }
return true
case .number:
// Compatible if it's a double, or if not strict and it's an int
switch self {
case .double: return true
case .int: return !strict
default: return false
}
case .integer:
// Check if the JSONValue is an int
guard case .int = self else { return false }
return true
case .boolean:
// Check if the JSONValue is a bool
guard case .bool = self else { return false }
return true
case .null:
// Check if the JSONValue is null
return self == .null
case .reference, .anyOf, .allOf, .oneOf, .not, .empty, .any:
// Compatibility for these types depends on deeper validation or context.
// Assume basic type compatibility is met, deferring to full validation.
return true
}
}
}
83 changes: 83 additions & 0 deletions Tests/JSONSchemaTests/JSONValueTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -414,3 +414,86 @@ import Testing
dict[.string("key")] = "value"
#expect(dict[.string("key")] == "value")
}

@Test func testJSONValueIsCompatible() {
// Test object compatibility
let objectValue: JSONValue = .object(["key": .string("value")])
#expect(objectValue.isCompatible(with: .object))
#expect(!objectValue.isCompatible(with: .array))
#expect(!objectValue.isCompatible(with: .string))
#expect(!objectValue.isCompatible(with: .number))
#expect(!objectValue.isCompatible(with: .integer))
#expect(!objectValue.isCompatible(with: .boolean))
#expect(!objectValue.isCompatible(with: .null))

// Test array compatibility
let arrayValue: JSONValue = .array([.int(1), .string("test")])
#expect(!arrayValue.isCompatible(with: .object))
#expect(arrayValue.isCompatible(with: .array))
#expect(!arrayValue.isCompatible(with: .string))
#expect(!arrayValue.isCompatible(with: .number))
#expect(!arrayValue.isCompatible(with: .integer))
#expect(!arrayValue.isCompatible(with: .boolean))
#expect(!arrayValue.isCompatible(with: .null))

// Test string compatibility
let stringValue: JSONValue = .string("test")
#expect(!stringValue.isCompatible(with: .object))
#expect(!stringValue.isCompatible(with: .array))
#expect(stringValue.isCompatible(with: .string))
#expect(!stringValue.isCompatible(with: .number))
#expect(!stringValue.isCompatible(with: .integer))
#expect(!stringValue.isCompatible(with: .boolean))
#expect(!stringValue.isCompatible(with: .null))

// Test number compatibility
let doubleValue: JSONValue = .double(3.14)
#expect(!doubleValue.isCompatible(with: .object))
#expect(!doubleValue.isCompatible(with: .array))
#expect(!doubleValue.isCompatible(with: .string))
#expect(doubleValue.isCompatible(with: .number))
#expect(!doubleValue.isCompatible(with: .integer))
#expect(!doubleValue.isCompatible(with: .boolean))
#expect(!doubleValue.isCompatible(with: .null))

// Test integer compatibility
let intValue: JSONValue = .int(42)
#expect(!intValue.isCompatible(with: .object))
#expect(!intValue.isCompatible(with: .array))
#expect(!intValue.isCompatible(with: .string))
#expect(intValue.isCompatible(with: .number, strict: false))
#expect(!intValue.isCompatible(with: .number, strict: true))
#expect(intValue.isCompatible(with: .integer))
#expect(!intValue.isCompatible(with: .boolean))
#expect(!intValue.isCompatible(with: .null))

// Test boolean compatibility
let boolValue: JSONValue = .bool(true)
#expect(!boolValue.isCompatible(with: .object))
#expect(!boolValue.isCompatible(with: .array))
#expect(!boolValue.isCompatible(with: .string))
#expect(!boolValue.isCompatible(with: .number))
#expect(!boolValue.isCompatible(with: .integer))
#expect(boolValue.isCompatible(with: .boolean))
#expect(!boolValue.isCompatible(with: .null))

// Test null compatibility
let nullValue: JSONValue = .null
#expect(!nullValue.isCompatible(with: .object))
#expect(!nullValue.isCompatible(with: .array))
#expect(!nullValue.isCompatible(with: .string))
#expect(!nullValue.isCompatible(with: .number))
#expect(!nullValue.isCompatible(with: .integer))
#expect(!nullValue.isCompatible(with: .boolean))
#expect(nullValue.isCompatible(with: .null))

// Test composite schema types (should always return true)
let anyValue: JSONValue = .string("any")
#expect(anyValue.isCompatible(with: .reference("$ref")))
#expect(anyValue.isCompatible(with: .anyOf([])))
#expect(anyValue.isCompatible(with: .allOf([])))
#expect(anyValue.isCompatible(with: .oneOf([])))
#expect(anyValue.isCompatible(with: .not(.string)))
#expect(anyValue.isCompatible(with: .empty))
#expect(anyValue.isCompatible(with: .any))
}