Migrating 2.1 Validators to 3.0


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. This necessitated some changes in the API for how you create custom validators.

Odin 3.0's validation system is fully backwards compatible with "legacy" validators implemented in Odin 2.1, but the legacy extension points are marked warning-level obsolete, and will be removed in a future major version upgrade of Odin. Therefore, it is better to migrate to the property-based validation extension points instead.

Luckily, this is quite easy, and will often result in less overall code! This is especially the case 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().

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 (if there is a value) and this.Attribute (if there is an 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 based on an old version of our custom validator tutorial:

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.ValueEntry.SmartValue, 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.ValueEntry.SmartValue))
        {
            result.ResultType = ValidationResultType.Warning;
            result.Message = "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, and then access our value through the value entry we have available.

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.ValueEntry.SmartValue);
        }
        catch (ArgumentException ex)
        {
            result.ResultType = ValidationResultType.Error;
            result.Message = "Invalid regex string: " + ex.Message;
        }
    }
}

And that's it!