Object Validation in Swift 3

There are many validation libraries for iOS but most of them are tied to the UI. The main goal here is to have something that can

  1. Abstract away from UI ✔️
  2. Extract validation logic and avoid polluting objects models structure with external libraries ✔️
  3. Validation errors ✔️ (localisable! 🎉 and customisable)

So I tried to put together the features coming from other libraries I’ve been working with in the past: FluentValidation and ActiveRecord

FluidValidator

The basic idea is to have a class that encapsulates all the validation logic. This class will extend AbstractValidator<T> where T is the target class that you want to validate. Let’s see some code.

Example Object

class Home {
var isLocked:Bool?
var number:Int?
var ownerName:String?
}

Let’s assume you want to validate this object and ensure that:

  • isLocked is true
  • number is greater than 3
  • ownerName is not empty.

The validation class would look like something as below

Example Validator

import FluidValidator

class HomeValidator : AbstractValidator<Home> {
override init() {
super.init()

self.addValidation(withName: "number") { (context) -> (Any?) in
context.number
}.addRule(GreaterThan(limit: 3, includeLimit: false))

self.addValidation(withName: "ownerName") { (context) -> (Any?) in
context.ownerName
}.addRule(BeNotEmpty())

self.addValidation(withName: "isLocked") { (context) -> (Any?) in
context.isLocked
}.addRule(BeTrue())
}
}

The breakdown


import FluidValidator

class HomeValidator : AbstractValidator<Home>

Import the library, declare a new class which extends AbstractValidator<T> where T is the target class you want to validate.

self.addValidation(withName: "number") { (context) -> (Any?) in
context.number
}.addRule(GreaterThan(limit: 3, includeLimit: false))

Here we are adding a new validation, in this case number.
addValidation method accepts a string representing the property (but it could even be an abstract state) and a block which accepts in input a reference to the object we are validating, in this case instance of Home.

Now we start adding rules for the newly created validation property

.GreaterThan(limit: 3, includeLimit: false)

Optionally we could chain as many rules as we need

.addRule(BeNotEmpty()).addRule(InRange(min:2, max: 9)).addRule(…)

That’s it

It’s a plain swift object which extends AbstractValidator<T>. It can be customised to accomodate any design need.
Let’s see what happens when running the validation

Running Validations

let home = Home()
home.isLocked = false
home.ownerName = "John Doe"
home.number = 2

let homeValidator = HomeValidator()
let result = homeValidator.validate(home)
let failMessage = homeValidator.allErrors

Reading the errors (if any)

failMessage.failMessageForPath("number")?.errors.first?.compact
failMessage.failMessageForPath("number")?.errors.first?.extended

Since it’s possible to attach multiple rules to each property, an array of errors is available. For each error there are two versions of message: compact and extended.

The number property is holding 2, so validation will fail generating following messages:
compact → “This needs to be greater than 3.”
extended → Home.number.error.name has to be greater than 3. 2 supplied.”

Customise error messages

The property name is build out of the Class name and property name you probably want to customise that. All you have to do is to create a localised string file called: “object_validator” and insert the following:

“Home.number.error.name” = “Number”;

Want to customise the whole error message? Add the following:

"GreaterThan.error.message" = "Hey man, this must be greater.";
"GreaterThan.error.message.extended" = "%1$@ must be greater than %2$@. You supplied %3$@.";

The failing message now will change to:

compact “Hey man, this must be greater.”
extended → “Number must be greater than 3. You supplied 2.”

Localisation

Since strings files can be localised, you can have a file for each language and the library will pick the right one according to the current language being used.

Nested objects? No problem

Let’s say you Home class has an additional property named garage of type Garage

class Home {
var isLocked:Bool?
var number:Int?
var ownerName:String?
var garage: Garage?
}
class Garage {
var isOpen: Bool?
var maxCars: Int?
}

We build a GarageValidator same as before

class GarageValidator: AbstractValidator<Garage> {
override init() {
super.init()

self.addValidation(withName: "isOpen") { (context) -> Any? in
context.isOpen
}.addRule(BeTrue())

self.addValidation(withName: "maxCars") { (context) -> Any? in
context.maxCars
}.addRule(LessThan(limit: 2, includeLimit: true))
}
}

Now easy enough all we have to do is pass the GarageValidator as validation rule for the garage property.

class HomeValidator : AbstractValidator<Home> {
override init() {
super.init()

...

self.addValidation(withName: "garage") { (context) -> (Any?) in
context.garage
}.addRule(GarageValidator())
}
}

Validation rules and Validators conform to the same protocol. It’s like a composite-ish pattern: we treat a single element and group of elements in the same way. This means you can have as many nested objects as you need.

Let’s suppose you have the following in the code somewhere:

let garage = Garage()
garage.isOpen = false
home.garage = garage

How do we read the errors in this case?

failMessage.failMessageForPath("garage")?.errors.first?.compact
//Home.garage.error.name object contains errors.
failMessage.failMessageForPath("garage.isOpen")?.errors.first?.extended
// Garage.isOpen.error.name must be active. not active passed

Enumerable properties

Suppose the Home class has another property of type Array<Floor>

class Home {
var isLocked:Bool?
var number:Int?
var ownerName:String?
var garage: Garage?
var floors: Array<Floor>?
}
class Floor {
var rooms: Int

init(withMaxRooms maxRooms: Int) {
rooms = maxRooms
}
}

For example let’s write a validator class for Floor type too…

class FloorValidator: AbstractValidator<Floor> {
override init() {
super.init()

self.addValidation("rooms") { (context) -> Any? in
context.rooms
}.addRule(GreaterThan(limit: 4, includeLimit: true))
}
}

Here is how the HomeValidator would change

class HomeValidator : AbstractValidator<Home> {
override init() {
super.init()
...
self.addValidation("floors") { (context) -> (Any?) in
context.floors
}.addRule(EnumeratorValidator(validatable: FloorValidator()))
}
}

The big difference is that we need to supply a wrapper to the addRule method to instruct the library to treat the contents of floors property as a List. Therefore

EnumeratorValidator(validatable: FloorValidator())

Let’s pretend again somewhere in the code we have

let floor_1 = Floor(withMaxRooms: 2)
let floor_2 = Floor(withMaxRooms: 3)
...
home.floors = [floor_1, floor_2]

How do we read the errors on an Array typed property?

failMessage.failMessageForPath("floors")?.errors.first?.compact
// Home.floors.error.name object contains errors.
failMessage.failMessageForPath("floors.0.rooms")?.errors.first?.extended
// Floor.rooms.error.name has to be greater than 4. 1 supplied

That’s all folks! 🏁

The project is still at the beginning and of course there’s plenty of room for improvement, so if anyone is interested in contributing, get in contact!

github: https://github.com/frograin/FluidValidator
cocoapod: https://cocoapods.org/pods/FluidValidator