Validators can be used in Popcorn apps to valid your json inputs before data processing and persistence.
Here an example with a Book management app. We use an ObjectValidator to validate
the books passed to the API in the POST /books handler.
import popcorn
import popcorn::pop_json
import serialization
# Serializable book representation.
class Book
    super Serializable
    # Book ISBN
    var isbn: String
    # Book title
    var title: String
    # Book image (optional)
    var img: nullable String
    # Book price
    var price: Float
end
class BookValidator
    super ObjectValidator
    redef init do
        add new ISBNField("isbn")
        add new StringField("title", min_size=1, max_size=255)
        add new StringField("img", required=false)
        add new FloatField("price", min=0.0, max=999.0)
    end
end
class BookHandler
    super Handler
    # Insert a new Book
    redef fun post(req, res) do
        var validator = new BookValidator
        if not validator.validate(req.body) then
            res.json(validator.validation, 400)
            return
        end
        # TODO data persistence
    end
endSerializable::inspect to show more useful information
			serialization :: serialization_core
Abstract services to serialize Nit objects to different formatscore :: union_find
union–find algorithm using an efficient disjoint-set data structure
# Quick and easy validation framework for Json inputs
#
# Validators can be used in Popcorn apps to valid your json inputs before
# data processing and persistence.
#
# Here an example with a Book management app. We use an ObjectValidator to validate
# the books passed to the API in the `POST /books` handler.
#
# ~~~
# import popcorn
# import popcorn::pop_json
# import serialization
#
# # Serializable book representation.
# class Book
#	super Serializable
#
#	# Book ISBN
#	var isbn: String
#
#	# Book title
#	var title: String
#
#	# Book image (optional)
#	var img: nullable String
#
#	# Book price
#	var price: Float
# end
#
# class BookValidator
#	super ObjectValidator
#
#	redef init do
#		add new ISBNField("isbn")
#		add new StringField("title", min_size=1, max_size=255)
#		add new StringField("img", required=false)
#		add new FloatField("price", min=0.0, max=999.0)
#	end
# end
#
# class BookHandler
#	super Handler
#
#	# Insert a new Book
#	redef fun post(req, res) do
#		var validator = new BookValidator
#		if not validator.validate(req.body) then
#			res.json(validator.validation, 400)
#			return
#		end
#		# TODO data persistence
#	end
# end
# ~~~
module pop_validation
import json::static
# The base class of all validators
abstract class DocumentValidator
	# Validation result
	#
	# Accessible to the client after the `validate` method has been called.
	var validation: ValidationResult is noinit
	# Validate the `document` input
	#
	# Result of the validation can be found in the `validation` attribute.
	fun validate(document: String): Bool do
		validation = new ValidationResult
		return true
	end
end
# Validation Result representation
#
# Can be convertted to a JsonObject so it can be reterned in a Json HttpResponse.
#
# Errors messages are grouped into *scopes*. A scope is a string that specify wich
# field or document the error message is related to.
class ValidationResult
	super Serializable
	# Object parsed during validation
	#
	# Can be used as a quick way to access the parsed JsonObject instead of
	# reparsing it during the answer.
	#
	# See `ObjectValidator`.
	var object: nullable JsonObject = null is writable
	# Array parsed during validation
	#
	# Can be used as a quick way to access the parsed JsonArray instead of
	# reparsing it during the answer.
	#
	# See `ArrayValidator`.
	var array: nullable JsonArray = null is writable
	# Errors found during validation
	#
	# Errors are grouped by scope.
	var errors = new HashMap[String, Array[String]]
	# Generate a new error `message` into `scope`
	fun add_error(scope, message: String) do
		if not errors.has_key(scope) then
			errors[scope] = new Array[String]
		end
		errors[scope].add message
	end
	# Get the errors for `scope`
	fun error(scope: String): Array[String] do
		if not errors.has_key(scope) then
			return new Array[String]
		end
		return errors[scope]
	end
	# Does `self` contains `errors`?
	fun has_error: Bool do return errors.not_empty
	redef fun core_serialize_to(v) do
		var errors = new JsonObject
		for k, e in self.errors do
			errors[k] = new JsonArray.from(e)
		end
		v.serialize_attribute("has_error", has_error)
		v.serialize_attribute("errors", errors)
	end
	# Returns the validation result as a pretty formated string
	fun to_pretty_string: String do
		var b = new Buffer
		if not has_error then
			b.append "Everything is correct\n"
		else
			b.append "There is errors\n\n"
			for k, v in errors do
				b.append "{k}:\n"
				for vv in v do
					b.append "\t{vv}\n"
				end
				b.append "\n"
			end
		end
		return b.write_to_string
	end
end
# Check a JsonObject
# ~~~
# var validator = new ObjectValidator
# validator.add new RequiredField("id", required = true)
# validator.add new StringField("login", min_size=4)
# validator.add new IntField("age", min=0, max=100)
# assert not validator.validate("""{}""")
# assert not validator.validate("""[]""")
# assert validator.validate("""{ "id": "", "login": "Alex", "age": 10 }""")
# ~~~
class ObjectValidator
	super DocumentValidator
	# Validators to apply on the object
	var validators = new Array[FieldValidator]
	redef fun validate(document) do
		super
		var json = document.parse_json
		if json == null then
			validation.add_error("document", "Expected JsonObject got `null`")
			return false
		end
		return validate_json(json)
	end
	# Validate a Serializable input
	fun validate_json(json: Serializable): Bool do
		if not json isa JsonObject then
			validation.add_error("document", "Expected JsonObject got `{json.class_name}`")
			return false
		end
		validation.object = json
		for validator in validators do
			var res = validator.validate_field(self, json)
			if not res then return false
		end
		return true
	end
	# Add a validator
	fun add(validator: FieldValidator) do validators.add validator
end
# Check a JsonArray
# ~~~
# var validator = new ArrayValidator
# assert not validator.validate("""{}""")
# assert validator.validate("""[]""")
# assert validator.validate("""[ "id", 10, {} ]""")
#
# validator = new ArrayValidator(allow_empty=false)
# assert not validator.validate("""[]""")
# assert validator.validate("""[ "id", 10, {} ]""")
#
# validator = new ArrayValidator(length=3)
# assert not validator.validate("""[]""")
# assert validator.validate("""[ "id", 10, {} ]""")
# ~~~
class ArrayValidator
	super DocumentValidator
	# Allow empty arrays (default: true)
	var allow_empty: nullable Bool
	# Check array length (default: no check)
	var length: nullable Int
	redef fun validate(document) do
		super
		var json = document.parse_json
		if json == null then
			validation.add_error("document", "Expected JsonArray got `null`")
			return false
		end
		return validate_json(json)
	end
	# Validate a Serializable input
	fun validate_json(json: Serializable): Bool do
		if not json isa JsonArray then
			validation.add_error("document", "Expected JsonArray got `{json.class_name}`")
			return false
		end
		validation.array = json
		var allow_empty = self.allow_empty
		if json.is_empty and (allow_empty != null and not allow_empty) then
			validation.add_error("document", "Cannot be empty")
			return false
		end
		var length = self.length
		if length != null and json.length != length then
			validation.add_error("document", "Array length must be exactly `{length}`")
			return false
		end
		return true
	end
end
# Something that can validate a JsonObject field
abstract class FieldValidator
	# Field to validate
	var field: String
	# Validate `field` in `obj`
	fun validate_field(v: ObjectValidator, obj: JsonObject): Bool is abstract
end
# Check if a field exists
#
# ~~~
# var json1 = """{ "field1": "", "field2": "foo", "field3": 10, "field4": [] }"""
# var json2 = """{ "field1": "", "field2": "foo", "field3": 10 }"""
# var json3 = """{ "field1": "", "field2": "foo" }"""
#
# var validator = new ObjectValidator
# validator.add new RequiredField("field1")
# validator.add new RequiredField("field2")
# validator.add new RequiredField("field3")
# validator.add new RequiredField("field4", required=false)
#
# assert validator.validate(json1)
# assert validator.validate(json2)
# assert not validator.validate(json3)
# assert validator.validation.error("field3") == ["Required field"]
# ~~~
class RequiredField
	super FieldValidator
	# Is this field required?
	var required: nullable Bool
	redef fun validate_field(v, obj) do
		var required = self.required
		if (required != null and required or required == null) and not obj.has_key(field) then
			v.validation.add_error(field, "Required field")
			return false
		end
		return true
	end
end
# Check if a field is a String
#
# `min_size` and `max_size` are optional
#
# ~~~
# var validator = new ObjectValidator
# validator.add new StringField("field", required=false)
# assert validator.validate("""{}""")
#
# validator = new ObjectValidator
# validator.add new StringField("field")
# assert not validator.validate("""{}""")
# assert not validator.validate("""{ "field": 10 }""")
#
# validator = new ObjectValidator
# validator.add new StringField("field", min_size=3)
# assert validator.validate("""{ "field": "foo" }""")
# assert not validator.validate("""{ "field": "fo" }""")
# assert not validator.validate("""{ "field": "" }""")
#
# validator = new ObjectValidator
# validator.add new StringField("field", max_size=3)
# assert validator.validate("""{ "field": "foo" }""")
# assert not validator.validate("""{ "field": "fooo" }""")
#
# validator = new ObjectValidator
# validator.add new StringField("field", min_size=3, max_size=5)
# assert not validator.validate("""{ "field": "fo" }""")
# assert validator.validate("""{ "field": "foo" }""")
# assert validator.validate("""{ "field": "foooo" }""")
# assert not validator.validate("""{ "field": "fooooo" }""")
# ~~~
class StringField
	super RequiredField
	# String min size (default: not checked)
	var min_size: nullable Int
	# String max size (default: not checked)
	var max_size: nullable Int
	redef fun validate_field(v, obj) do
		if not super then return false
		var val = obj.get_or_null(field)
		if val == null then
			if required == null or required == true then
				v.validation.add_error(field, "Expected String got `null`")
				return false
			else
				return true
			end
		end
		if not val isa String then
			v.validation.add_error(field, "Expected String got `{val.class_name}`")
			return false
		end
		var min_size = self.min_size
		if min_size != null and val.length < min_size then
			v.validation.add_error(field, "Must be at least `{min_size} characters long`")
			return false
		end
		var max_size = self.max_size
		if max_size != null and val.length > max_size then
			v.validation.add_error(field, "Must be at max `{max_size} characters long`")
			return false
		end
		return true
	end
end
# Check if a field is an Int
#
# ~~~
# var validator = new ObjectValidator
# validator.add new IntField("field", required=false)
# assert validator.validate("""{}""")
#
# validator = new ObjectValidator
# validator.add new IntField("field")
# assert not validator.validate("""{}""")
# assert not validator.validate("""{ "field": "foo" }""")
# assert validator.validate("""{ "field": 10 }""")
#
# validator = new ObjectValidator
# validator.add new IntField("field", min=3)
# assert validator.validate("""{ "field": 3 }""")
# assert not validator.validate("""{ "field": 2 }""")
#
# validator = new ObjectValidator
# validator.add new IntField("field", max=3)
# assert validator.validate("""{ "field": 3 }""")
# assert not validator.validate("""{ "field": 4 }""")
#
# validator = new ObjectValidator
# validator.add new IntField("field", min=3, max=5)
# assert not validator.validate("""{ "field": 2 }""")
# assert validator.validate("""{ "field": 3 }""")
# assert validator.validate("""{ "field": 5 }""")
# assert not validator.validate("""{ "field": 6 }""")
# ~~~
class IntField
	super RequiredField
	# Min value (default: not checked)
	var min: nullable Int
	# Max value (default: not checked)
	var max: nullable Int
	redef fun validate_field(v, obj) do
		if not super then return false
		var val = obj.get_or_null(field)
		if val == null then
			if required == null or required == true then
				v.validation.add_error(field, "Expected Int got `null`")
				return false
			else
				return true
			end
		end
		if not val isa Int then
			v.validation.add_error(field, "Expected Int got `{val.class_name}`")
			return false
		end
		var min = self.min
		if min != null and val < min then
			v.validation.add_error(field, "Must be greater or equal to `{min}`")
			return false
		end
		var max = self.max
		if max != null and val > max then
			v.validation.add_error(field, "Must be smaller or equal to `{max}`")
			return false
		end
		return true
	end
end
# Check if a field is a Float
#
# ~~~
# var validator = new ObjectValidator
# validator.add new FloatField("field", required=false)
# assert validator.validate("""{}""")
#
# validator = new ObjectValidator
# validator.add new FloatField("field")
# assert not validator.validate("""{}""")
# assert not validator.validate("""{ "field": "foo" }""")
# assert validator.validate("""{ "field": 10.5 }""")
#
# validator = new ObjectValidator
# validator.add new FloatField("field", min=3.0)
# assert validator.validate("""{ "field": 3.0 }""")
# assert not validator.validate("""{ "field": 2.0 }""")
#
# validator = new ObjectValidator
# validator.add new FloatField("field", max=3.0)
# assert validator.validate("""{ "field": 3.0 }""")
# assert not validator.validate("""{ "field": 4.0 }""")
#
# validator = new ObjectValidator
# validator.add new FloatField("field", min=3.0, max=5.0)
# assert not validator.validate("""{ "field": 2.0 }""")
# assert validator.validate("""{ "field": 3.0 }""")
# assert validator.validate("""{ "field": 5.0 }""")
# assert not validator.validate("""{ "field": 6.0 }""")
# ~~~
class FloatField
	super RequiredField
	# Min value (default: not checked)
	var min: nullable Float
	# Max value (default: not checked)
	var max: nullable Float
	redef fun validate_field(v, obj) do
		if not super then return false
		var val = obj.get_or_null(field)
		if val == null then
			if required == null or required == true then
				v.validation.add_error(field, "Expected Float got `null`")
				return false
			else
				return true
			end
		end
		if not val isa Float then
			v.validation.add_error(field, "Expected Float got `{val.class_name}`")
			return false
		end
		var min = self.min
		if min != null and val < min then
			v.validation.add_error(field, "Must be smaller or equal to `{min}`")
			return false
		end
		var max = self.max
		if max != null and val > max then
			v.validation.add_error(field, "Must be greater or equal to `{max}`")
			return false
		end
		return true
	end
end
# Check if a field is a Bool
#
# ~~~
# var validator = new ObjectValidator
# validator.add new BoolField("field", required=false)
# assert validator.validate("""{}""")
# assert validator.validate("""{ "field": true }""")
# assert validator.validate("""{ "field": false }""")
# assert not validator.validate("""{ "field": "foo" }""")
#
# validator = new ObjectValidator
# validator.add new BoolField("field")
# assert not validator.validate("""{}""")
# assert validator.validate("""{ "field": true }""")
# assert validator.validate("""{ "field": false }""")
# assert not validator.validate("""{ "field": "foo" }""")
# ~~~
#
# No type conversion is applied on the input value:
# ~~~
# assert not validator.validate("""{ "field": "true" }""")
# assert not validator.validate("""{ "field": 1 }""")
# assert not validator.validate("""{ "field": [true] }""")
# ~~~
class BoolField
	super RequiredField
	redef fun validate_field(v, obj) do
		if not super then return false
		var val = obj.get_or_null(field)
		if val == null then
			if required == null or required == true then
				v.validation.add_error(field, "Expected Bool got `null`")
				return false
			else
				return true
			end
		end
		if not val isa Bool then
			v.validation.add_error(field, "Expected Bool got `{val.class_name}`")
			return false
		end
		return true
	end
end
# Check that a field is a JsonObject
#
# ~~~
# var validator = new ObjectValidator
# validator.add new RequiredField("id", required = true)
# var user_val = new ObjectField("user")
# user_val.add new RequiredField("id", required = true)
# user_val.add new StringField("login", min_size=4)
# validator.add user_val
# assert not validator.validate("""{ "id": "", "user": { "login": "Alex" } }""")
# assert validator.validate("""{ "id": "", "user": { "id": "foo", "login": "Alex" } }""")
# ~~~
class ObjectField
	super RequiredField
	super ObjectValidator
	redef var validation = new ValidationResult
	redef fun validate_field(v, obj) do
		if not super then return false
		var val = obj.get_or_null(field)
		if val == null then
			if required == null or required == true then
				v.validation.add_error(field, "Expected Object got `null`")
				return false
			else
				return true
			end
		end
		var res = validate_json(val)
		for field, messages in validation.errors do
			for message in messages do v.validation.add_error("{self.field}.{field}", message)
		end
		return res
	end
end
# Check that a field is a JsonArray
#
# ~~~
# var validator = new ObjectValidator
# validator.add new RequiredField("id", required = true)
# validator.add new ArrayField("orders", allow_empty=false)
# assert not validator.validate("""{ "id": "", "orders": [] }""")
# assert validator.validate("""{ "id": "", "orders": [ 1 ] }""")
# ~~~
class ArrayField
	super RequiredField
	super ArrayValidator
	autoinit field=, required=, allow_empty=, length=
	redef var validation = new ValidationResult
	redef fun validate_field(v, obj) do
		if not super then return false
		var val = obj.get_or_null(field)
		if val == null then
			if required == null or required == true then
				v.validation.add_error(field, "Expected Array got `null`")
				return false
			else
				return true
			end
		end
		var res = validate_json(val)
		for field, messages in validation.errors do
			for message in messages do v.validation.add_error("{self.field}.{field}", message)
		end
		return res
	end
end
# Check if two fields values match
#
# ~~~
# var validator = new ObjectValidator
# validator.add new FieldsMatch("field1", "field2")
#
# assert validator.validate("""{ "field1": {}, "field2": {} }""")
# assert validator.validate("""{ "field1": "foo", "field2": "foo" }""")
# assert validator.validate("""{ "field1": null, "field2": null }""")
# assert validator.validate("""{}""")
#
# assert not validator.validate("""{ "field1": {}, "field2": [] }""")
# assert not validator.validate("""{ "field1": "foo", "field2": "bar" }""")
# assert not validator.validate("""{ "field1": null, "field2": "" }""")
# assert not validator.validate("""{ "field1": "foo" }""")
# ~~~
class FieldsMatch
	super FieldValidator
	# Other field to compare with
	var other: String
	redef fun validate_field(v, obj) do
		var val1 = obj.get_or_null(field)
		var val2 = obj.get_or_null(other)
		if val1 != val2 then
			v.validation.add_error(field, "Values mismatch: `{val1 or else "null"}` against `{val2 or else "null"}`")
			return false
		end
		return true
	end
end
# Check if a field match a regular expression
#
# ~~~
# var validator = new ObjectValidator
# validator.add new RegexField("title", "[A-Z][a-z]+".to_re)
# assert not validator.validate("""{ "title": "foo" }""")
# assert validator.validate("""{ "title": "Foo" }""")
# ~~~
class RegexField
	super RequiredField
	autoinit field, re, required
	# Regular expression to match
	var re: Regex
	redef fun validate_field(v, obj) do
		if not super then return false
		var val = obj.get_or_null(field)
		if val == null then
			if required == null or required == true then
				v.validation.add_error(field, "Expected String got `null`")
				return false
			else
				return true
			end
		end
		if not val isa String then
			v.validation.add_error(field, "Expected String got `{val.class_name}`")
			return false
		end
		if not val.has(re) then
			v.validation.add_error(field, "Does not match `{re.string}`")
			return false
		end
		return true
	end
end
# Check if a field is a valid email
#
# ~~~
# var validator = new ObjectValidator
# validator.add new EmailField("email")
# assert not validator.validate("""{ "email": "" }""")
# assert not validator.validate("""{ "email": "foo" }""")
# assert validator.validate("""{ "email": "alexandre@moz-code.org" }""")
# assert validator.validate("""{ "email": "a+b@c.d" }""")
# ~~~
class EmailField
	super RegexField
	autoinit field, required
	redef var re = "(^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\\.[a-zA-Z0-9.-]+$)".to_re
end
# Check if a field is a valid ISBN
#
# ~~~
# var validator = new ObjectValidator
# validator.add new ISBNField("isbn")
# assert not validator.validate("""{ "isbn": "foo" }""")
# assert validator.validate("""{ "isbn": "ISBN 0-596-00681-0" }""")
# ~~~
class ISBNField
	super RegexField
	autoinit field, required
	redef var re = "(^ISBN [0-9]-[0-9]\{3\}-[0-9]\{5\}-[0-9]?$)".to_re
end
# Check if a field is a valid URL
#
# Matched against the following regular expression:
# ~~~raw
# ^(http|https):\/\/[a-zA-Z0-9\-_]+(\.[a-zA-Z0-9\-_]+)+([a-zA-Z0-9\-\.,@?^=%&:/~\+#]*[a-zA-Z0-9\-\@?^=%&/~\+#])?
# ~~~
# You should redefine the base regular expression `re` with your own.
#
# ~~~
# var validator = new ObjectValidator
# validator.add new URLField("url")
# assert not validator.validate("""{ "url": "" }""")
# assert not validator.validate("""{ "url": "foo" }""")
# assert not validator.validate("""{ "url": "http://foo" }""")
# assert validator.validate("""{ "url": "http://nitlanguage.org" }""")
# assert validator.validate("""{ "url": "http://nitlanguage.org/foo" }""")
# assert validator.validate("""{ "url": "http://nitlanguage.org/foo?q" }""")
# assert validator.validate("""{ "url": "http://nitlanguage.org/foo?q&a" }""")
# assert validator.validate("""{ "url": "http://nitlanguage.org/foo?q&a=1" }""")
# ~~~
class URLField
	super RegexField
	autoinit field, required
	redef var re = "^(http|https):\\/\\/[a-zA-Z0-9\\-_]+(\\.[a-zA-Z0-9\\-_]+)+([a-zA-Z0-9\\-\\.,@?^=%&:/~\\+#]*[a-zA-Z0-9\\-\\@?^=%&/~\\+#])?".to_re
end
# Check if a field value is already used
#
# This class provides a stub validator for fields that should contain a unique value along an
# application (typically logins or ids).
#
# Here an example that uses a `Repository` if an email is unique:
# ~~~nitish
# class UniqueEmailField
#	super UniqueField
#
#	var users: UsersRepository
#
#	redef fun check_unicity(v, field, val) do
#		var user = users.find_by_email(val)
#		if user != null then
#			v.validation.add_error(field, "Email `{val}` already used")
#			return false
#		end
#		return true
#	end
# end
# ~~~
class UniqueField
	super StringField
	# Check if `val` is already used somewhere
	#
	# You must redefine this method to handle your own validation.
	fun check_unicity(v: ObjectValidator, field, val: String): Bool is abstract
	redef fun validate_field(v, obj) do
		if not super then return false
		var val = obj.get_or_null(field)
		if not val isa String then return false
		if not check_unicity(v, field, val) then return false
		return true
	end
end
lib/popcorn/pop_validation.nit:17,1--810,3