Skip to main content
Version: Next

Schema validations

The main goal of schemas is to validate data and request parameters. As such, schemas provide a convenient mechanism allowing to define validation rules. These rules can be inherited from the fields in your schema depending on the options you used and the type of your fields. They can also be explicitly specified in your schema class, which is useful if you need to implement custom validation logics.

Overview

A short example

Let's consider the following example:

class UserSchema < Marten::Schema
field :name, :string, required: true, max_size: 128
end

In the above snippet, a UserSchema schema is defined and it is specified that the name field must be present (required: true) and that the associated value cannot exceed 128 characters.

Given these characteristics, it is possible to initialize UserSchema instances and validate data through the use of the #valid? method:

schema_1 = UserSchema.new(Marten::Schema::DataHash.new)
schema_1.valid? # => false

schema_2 = UserSchema.new(Marten::Schema::DataHash{ "name" => "0" * 200) })
schema_2.valid? # => false

schema_3 = UserSchema.new(Marten::Schema::DataHash{ "name" => "John Doe") })
schema_3.valid? # => true

As you can see in the above examples, the first two schemas are invalid because either the name field is not specified or because its value exceeds the maximum characters limit. The last schema is valid though because it has a name field that is less than 128 characters.

Running schema validations

As highlighted in the previous section, schema validation rules will be executed when calling the #valid? and #invalid? methods: these methods return true or false depending on whether the data is valid or invalid.

schema = UserSchema.new(Marten::Schema::DataHash.new)
schema.valid? # => false
schema.invalid? # => true

Field validation rules

As mentioned previously, fields can contribute validation rules to your schemas. These validation rules can be inherited:

  • from the field type itself: some fields will validate that values are of a specific type (for example a uuid field will not validate values that don't correspond to valid UUIDs)
  • from the field options you define (for example fields using required: true will result in errors if the field is missing from the validated data)

Please refer to the fields reference to learn more about the supported field types and their associated options.

Custom validation rules

Custom validation rules can be defined through the use of the #validate macro. This macro lets you configure the name of a validation method that should be called when a schema instance is validated. Inside this method, you can implement any validation logic that you might require and add errors to the schema instance if the data is invalid.

For example:

class SignUpSchema < Marten::Schema
field :email, :string, max_size: 254
field :password1, :string, max_size: 128, strip: false
field :password2, :string, max_size: 128, strip: false

validate :validate_password

def validate_password
return unless validated_data["password1"]? && validated_data["password2"]?

if validated_data["password1"] != validated_data["password2"]
errors.add("The two password fields do not match")
end
end
end

In the above snippet, a custom validation method ensures that the password1 and password2 fields have the exact same value. If that's not the case, then a specific error (that is not associated with any fields) is added to the schema instance (which makes it invalid). It's interesting to note the use of the #validated_data method here: this method returns a hash of all the values that were previously sanitized and validated. You can make use of it when defining custom validation rules: indeed, these rules always run after all the fields have been individually validated first.

important

You can define multiple validation rules in your schema classes. When doing so, don't forget that these custom validation rules are called in the order they are defined.

Validation errors

Methods like #valid? or #invalid? only let you know whether a schema instance is valid or invalid for a specific data set. But you'll likely want to know exactly what are the actual errors or how to add new ones.

As such, every schema instance has an associated error set, which is an instance of Marten::Core::Validation::ErrorSet.

Inspecting errors

A schema instance error set lets you access all the errors of a specific schema instance. For example:

schema = UserSchema.new(Marten::Schema::DataHash.new)

schema.valid?
# => false

schema.errors.size
# => 1

schema.errors
# => #<Marten::Core::Validation::ErrorSet:0x100e1b740
# @errors=
# [#<Marten::Core::Validation::Error:0x100db75d0
# @field="name",
# @message="This field is required.",
# @type="required">]>

As you can see, the error set gives you the ability to know how many errors are affecting your schema instance. Each error provides some additional information as well:

  • the associated field name (which can be nil if the error is global)
  • the error message
  • the error type, which is optional (required in the previous example)

You can also access the errors that are associated with a specific field very easily by using the #[] method:

schema.errors[:name]
# => [#<Marten::Core::Validation::Error:0x104fb75d0
# @field="name",
# @message="This field is required.",
# @type="required">]

Global errors (errors affecting the whole schema instances or multiple fields at once) can be listed through the use of the #global method.

Adding errors

Errors can be added to an error set through the use of the #add method. This method takes a field name, a message, and an optional error type:

schema.errors.add(:name, "Name is invalid")                      # error type is "invalid"
schema.errors.add(:name, "Name is invalid", type: :invalid_name) # error type is "invalid_name"

Global errors can be specified through the use of an alternative #add method that doesn't take a field name:

schema.errors.add("User is invalid")                      # error type is "invalid"
schema.errors.add("User is invalid", type: :invalid_user) # error type is "invalid_user"