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
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -319,7 +319,8 @@ struct RegistrationView: View {
| `PostalCodeValidationRule` | Validates postal/ZIP codes for different countries | `PostalCodeValidationRule(country: .uk, error: "Invalid post code")`
| `Base64ValidationRule` | | `Base64ValidationRule(error: "The input is not valid Base64.")`
| `UUIDValidationRule` | Validates UUID format | `UUIDValidationRule(error: "Please enter a valid UUID")` |

| `JSONValidationRule` | Validates that a string represents valid JSON | `JSONValidationRule(error: "Invalid JSON")`

## Custom Validators

Create custom validation rules by conforming to `IValidationRule`:
Expand Down
71 changes: 71 additions & 0 deletions Sources/ValidatorCore/Classes/Rules/JSONValidationRule.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
//
// Validator
// Copyright © 2026 Space Code. All rights reserved.
//

import Foundation

/// Validates that a string represents valid JSON.
///
/// # Example:
/// ```swift
/// let rule = JSONValidationRule(error: "Invalid JSON")
/// rule.validate(input: "{\"key\": \"value\"}") // true
/// rule.validate(input: "not json") // false
/// ```
public struct JSONValidationRule: IValidationRule {
// MARK: Types

public typealias Input = String

// MARK: Properties

/// The validation error returned if the input is not valid JSON.
public let error: IValidationError

// MARK: Initialization

/// Initializes a JSON validation rule.
///
/// - Parameter error: The validation error returned if input fails validation.
public init(error: IValidationError) {
self.error = error
}

// MARK: IValidationRule

public func validate(input: String) -> Bool {
guard !input.isEmpty else { return false }
guard let data = input.data(using: .utf8) else { return false }

do {
_ = try JSONSerialization.jsonObject(with: data, options: [])
} catch {
return false
}

return isStrictJSON(input)
}

// MARK: Private Methods

/// Validates strict JSON syntax to catch issues like trailing commas.
private func isStrictJSON(_ input: String) -> Bool {
let trimmed = input.trimmingCharacters(in: .whitespacesAndNewlines)

let trailingCommaPatterns = [
",\\s*}",
",\\s*\\]",
]

for pattern in trailingCommaPatterns where trimmed.range(of: pattern, options: .regularExpression) != nil {
return false
}

if trimmed.range(of: ",,", options: .literal) != nil {
return false
}

return true
}
}
16 changes: 8 additions & 8 deletions Sources/ValidatorCore/Classes/Rules/URLValidationRule.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,14 @@

import Foundation

/// Validates that a string represents a valid URL.
///
/// # Example:
/// ```swift
/// let rule = URLValidationRule(error: "Invalid URL")
/// rule.validate(input: "https://example.com") // true
/// rule.validate(input: "not_a_url") // false
/// ```
// Validates that a string represents a valid URL.
//
// # Example:
// ```swift
// let rule = URLValidationRule(error: "Invalid URL")
// rule.validate(input: "https://example.com") // true
// rule.validate(input: "not_a_url") // false
// ```
public struct URLValidationRule: IValidationRule {
// MARK: Types

Expand Down
1 change: 1 addition & 0 deletions Sources/ValidatorCore/Validator.docc/Overview.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ ValidatorCore contains all core validation rules, utilities, and mechanisms for
- ``PostalCodeValidationRule``
- ``Base64ValidationRule``
- ``UUIDValidationRule``
- ``JSONValidationRule``

### Articles

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,189 @@
//
// Validator
// Copyright © 2026 Space Code. All rights reserved.
//

import ValidatorCore
import XCTest

// MARK: - JSONValidationRuleTests

final class JSONValidationRuleTests: XCTestCase {
// MARK: - Properties

private var sut: JSONValidationRule!

// MARK: - Setup

override func setUp() {
super.setUp()
sut = JSONValidationRule(error: String.error)
}

override func tearDown() {
sut = nil
super.tearDown()
}

// MARK: - Tests

func test_validate_validJSONObject_shouldReturnTrue() {
// given
let json = "{\"key\": \"value\"}"

// when
let result = sut.validate(input: json)

// then
XCTAssertTrue(result)
}

func test_validate_validJSONArray_shouldReturnTrue() {
// given
let json = "[1, 2, 3]"

// when
let result = sut.validate(input: json)

// then
XCTAssertTrue(result)
}

func test_validate_validNestedJSON_shouldReturnTrue() {
// given
let json = "{\"user\": {\"name\": \"John\", \"age\": 30}}"

// when
let result = sut.validate(input: json)

// then
XCTAssertTrue(result)
}

func test_validate_validJSONWithNumbers_shouldReturnTrue() {
// given
let json = "{\"count\": 42, \"price\": 19.99}"

// when
let result = sut.validate(input: json)

// then
XCTAssertTrue(result)
}

func test_validate_validJSONWithBooleans_shouldReturnTrue() {
// given
let json = "{\"active\": true, \"deleted\": false}"

// when
let result = sut.validate(input: json)

// then
XCTAssertTrue(result)
}

func test_validate_validJSONWithNull_shouldReturnTrue() {
// given
let json = "{\"value\": null}"

// when
let result = sut.validate(input: json)

// then
XCTAssertTrue(result)
}

func test_validate_invalidJSON_shouldReturnFalse() {
// given
let json = "{key: value}"

// when
let result = sut.validate(input: json)

// then
XCTAssertFalse(result)
}

func test_validate_incompleteBraces_shouldReturnFalse() {
// given
let json = "{\"key\": \"value\""

// when
let result = sut.validate(input: json)

// then
XCTAssertFalse(result)
}

func test_validate_trailingComma_shouldReturnFalse() {
// given
let json = "{\"key\": \"value\",}"

// when
let result = sut.validate(input: json)

// then
XCTAssertFalse(result)
}

func test_validate_plainText_shouldReturnFalse() {
// given
let json = "not json"

// when
let result = sut.validate(input: json)

// then
XCTAssertFalse(result)
}

func test_validate_emptyString_shouldReturnFalse() {
// given
let json = ""

// when
let result = sut.validate(input: json)

// then
XCTAssertFalse(result)
}

func test_validate_whitespaceString_shouldReturnFalse() {
// given
let json = " "

// when
let result = sut.validate(input: json)

// then
XCTAssertFalse(result)
}

func test_validate_singleQuotesJSON_shouldReturnFalse() {
// given
let json = "{'key': 'value'}"

// when
let result = sut.validate(input: json)

// then
XCTAssertFalse(result)
}

func test_validate_missingQuotesOnKey_shouldReturnFalse() {
// given
let json = "{key: \"value\"}"

// when
let result = sut.validate(input: json)

// then
XCTAssertFalse(result)
}
}

// MARK: Constants

private extension String {
static let error = "JSON is invalid"
}
Loading