Migrating 2.1 and 3.0 Validators to 3.1

In Odin 3.0, the validation system was switched from running on its own custom, hard-coded reflection-based backend that worked directly with the members and values involved, to running on Odin's more powerful InspectorProperty system. In Odin 3.1, changes were further made with the release of the new rework of Odin Validator that reworked how results were registered and what they could contain. This all necessitated some changes in the API for how you create custom validators.

Odin 3.1's validation API is fully backwards compatible with validators implemented in Odin 3.0, but the legacy methods which were obsoleted with Odin 3.0 were removed with 3.1 and will now cause compiler errors.

Luckily, migrating is quite easy, and will often result in less overall code! This is especially the case for validators that before 3.0 made heavy use of reflection to do their job, as the switch to the property system gives validators access to value resolvers for handling such requirements. Several of Odin's built-in validators, such as the ValidateInput implementation, went from many hundreds of lines of code to only 70 lines, and grew more feature-rich as well.

These are the primary changes that needs to be accounted for:

  • The Validate(TValue value, ValidationResult result), Validate(object parentInstance, object memberValue, MemberInfo member, ValidationResult result) and Validate(object parentInstance, TValue memberValue, MemberInfo member, ValidationResult result) overloads have been replaced by the Validate(ValidationResult result) overload
  • The RunValueValidation() and RunMemberValidation() methods have been replaced by the RunValidation() method.
  • The Initialize(Type type) and Initialize(MemberInfo member, Type memberValueType) overloads have been replaced by the Initialize() overload.
  • The CanValidateValues(), CanValidateMembers(), CanValidateValue() and CanValidateMember() methods have been replaced by CanValidateProperty().
  • Errors and warnings are now added to ValidationResult via result.AddError("error") and result.AddWarning("warning"), and issues added this way can be further configured via a builder pattern to add buttons, metadata, fixes, scene GUI and more.

Instead of necessary contextual values being passed down to the validator through the method arguments, then much like drawers, the validator can now access contextual values through this.Property, this.ValueEntry, this.Value and this.Attribute. In short, validators are far simpler to make, there are fewer overloads to keep track of, and access to the property system greatly reduces the need for boilerplate code.

Let's look at a legacy value validator:

using Sirenix.OdinInspector.Editor.Validation;

[assembly: RegisterValidator(typeof(EmptyStringValidator))]

public class EmptyStringValidator : ValueValidator<string>
{
    protected override void Validate(string value, ValidationResult result)
    {
        if (string.IsNullOrEmpty(value))
        {
            result.ResultType = ValidationResultType.Warning;
            result.Message = "This string is empty! Are you sure that's correct?";
        }
    }
}

Like most custom validators, this validator is very easy to update to the 3.0 way of doing things. All we need to do is remove the string value parameter, and instead retrieve the current value through this.Value, just as we would in a value drawer:

using Sirenix.OdinInspector.Editor.Validation;

[assembly: RegisterValidator(typeof(EmptyStringValidator))]

public class EmptyStringValidator : ValueValidator<string>
{
    protected override void Validate(ValidationResult result)
    {
        if (string.IsNullOrEmpty(this.Value))
        {
            result.AddWarning("This string is empty! Are you sure that's correct?");
        }
    }
}

Let's take another example, also based on an old version of our custom validator tutorial:

using System;
using System.Reflection;
using System.Text.RegularExpressions;
using Sirenix.OdinInspector.Editor.Validation;

[assembly: RegisterValidator(typeof(RegexValidator))]

public class RegexAttribute : Attribute { }

public class RegexValidator : AttributeValidator<RegexAttribute, string>
{
    protected override void Validate(object parentInstance, TValue memberValue, MemberInfo member, ValidationResult result)
    {
        try
        {
            Regex.Match("", memberValue);
        }
        catch (ArgumentException ex)
        {
            result.ResultType = ValidationResultType.Error;
            result.Message = "Invalid regex string: " + ex.Message;
        }
    }
}

Again, we just need to adjust the overload so we're overriding the correct and far simpler Validate(ValidationResult result) overload, access our value through the new inherited members we have available, and use the new, easier method to register an error.

using System;
using System.Reflection;
using System.Text.RegularExpressions;
using Sirenix.OdinInspector.Editor.Validation;

[assembly: RegisterValidator(typeof(RegexValidator))]

public class RegexAttribute : Attribute { }

public class RegexValidator : AttributeValidator<RegexAttribute, string>
{
    protected override void Validate(ValidationResult result)
    {
        try
        {
            Regex.Match("", this.Value);
        }
        catch (ArgumentException ex)
        {
            result.AddError("Invalid regex string: " + ex.Message);
        }
    }
}

And that's it!