diff --git a/Sources/Vapor/Validation/Validation.swift b/Sources/Vapor/Validation/Validation.swift index 632d808a66..fe1caf6535 100644 --- a/Sources/Vapor/Validation/Validation.swift +++ b/Sources/Vapor/Validation/Validation.swift @@ -29,9 +29,17 @@ public struct Validation { self.init { container in let result: ValidatorResult do { - let nested = try container.nestedContainer(keyedBy: ValidationKey.self, forKey: key) - let results = validations.validate(nested) - result = ValidatorResults.Nested(results: results.results) + if container.contains(key), !required, try container.decodeNil(forKey: key) { + result = ValidatorResults.Skipped() + } else if container.contains(key) { + let nested = try container.nestedContainer(keyedBy: ValidationKey.self, forKey: key) + let results = validations.validate(nested) + result = ValidatorResults.Nested(results: results.results) + } else if required { + result = ValidatorResults.Missing() + } else { + result = ValidatorResults.Skipped() + } } catch { result = ValidatorResults.Codable(error: error) } diff --git a/Tests/VaporTests/ValidationTests.swift b/Tests/VaporTests/ValidationTests.swift index b8239fb28a..03add7e14f 100644 --- a/Tests/VaporTests/ValidationTests.swift +++ b/Tests/VaporTests/ValidationTests.swift @@ -16,6 +16,7 @@ class ValidationTests: XCTestCase { "name": "Zizek", "age": 3 }, + "favoritePet": null, "isAdmin": true } """ @@ -80,6 +81,53 @@ class ValidationTests: XCTestCase { XCTAssertEqual("\(error)", "isAdmin is not a(n) Bool") } + + let validOptionalFavoritePet = """ + { + "name": "Tanner", + "age": 24, + "gender": "male", + "email": "me@tanner.xyz", + "luckyNumber": 5, + "profilePictureURL": "https://foo.jpg", + "preferredColors": ["blue"], + "pet": { + "name": "Zizek", + "age": 3 + }, + "favoritePet": { + "name": "Zizek", + "age": 3 + }, + "isAdmin": true + } + """ + XCTAssertNoThrow(try User.validate(json: validOptionalFavoritePet)) + + let invalidOptionalFavoritePet = """ + { + "name": "Tanner", + "age": 24, + "gender": "male", + "email": "me@tanner.xyz", + "luckyNumber": 5, + "profilePictureURL": "https://foo.jpg", + "preferredColors": ["blue"], + "pet": { + "name": "Zizek", + "age": 3 + }, + "favoritePet": { + "name": "Zi!zek", + "age": 3 + }, + "isAdmin": true + } + """ + XCTAssertThrowsError(try User.validate(json: invalidOptionalFavoritePet)) { error in + XCTAssertEqual("\(error)", + "favoritePet name contains '!' (allowed: whitespace, A-Z, a-z, 0-9)") + } } func testCatchError() throws { @@ -310,6 +358,7 @@ private final class User: Validatable, Codable { var gender: Gender var email: String? var pet: Pet + var favoritePet: Pet? var luckyNumber: Int? var profilePictureURL: String? var preferredColors: [String] @@ -358,6 +407,12 @@ private final class User: Validatable, Codable { is: .count(5...) && .characterSet(.alphanumerics + .whitespaces)) pet.add("age", as: Int.self, is: .range(3...)) } + // optional favorite pet validations + v.add("favoritePet", required: false) { pet in + pet.add("name", as: String.self, + is: .count(5...) && .characterSet(.alphanumerics + .whitespaces)) + pet.add("age", as: Int.self, is: .range(3...)) + } v.add("isAdmin", as: Bool.self) } }