Mapping error messages from server to client
Apr 8, 2017
6 minute read

The web projects I’ve built in recent history have all been React SPAs backed by an ASP.NET Core API. SPAs make it easy to communicate rich data structures to an API. In earlier days, a feature that allows a user to create an Order and add a collection of OrderItems might have been implemented as two separate pages which submit their respective entities as a flat set of properties using an HTML form. Today, such a feature might be implemented as a single page which submits the entire Order + OrderItems object hierarchy as a single JSON object. This raises some challenges when we want to map error message from the server back to specific properties on the client.

The challenge I’ve come across is when there is a parent object (an Order) which contains a list of related items (OrderItems) which may have validation errors. It can be difficult to map error messages that came from the API back to the respective entities and inputs on the client. This is important if we want to mark inputs as being invalid or to position error messages near the invalid inputs.

I currently use FluentValidation to perform server-side validation. It does provide precise property metadata like "order.orderItem[4].quantity" with each validation error but I feel that parsing such metadata on the client is a less robust solution to error mapping.

The solution I’ve settled on takes each error message, groups them by a temporary guid assigned to each entity by the client, then finally groups them by a canonicalised property name. The client can then map error messages using a simple process: When a field is being rendered, check if the guid of the field’s entity is present in the error dictionary. If it is, check if the property name is also present. If so, the field is in error. The key to the simplicity of this is having a guid for every entity in the submitted object hierarchy. For example, Order and each of its OrderItems would have their own guid.

Here’s how this is implemented as a few extension methods for FluentValidator and a bind method used in a React component’s render method:

First we set up an IHasGuid interface. Every submitted entity should implement this:

public interface IHasGuid
{
    Guid Guid { get; }
}

Then we add a FluentValidation extension method to allow us to plug the entity guid into the CustomState field of a ValidationError:

public static IRuleBuilderOptions<TModel, TReturn> WithGuid<TModel, TReturn>(
    this IRuleBuilderOptions<TModel, TReturn> builder) where TModel : IHasGuid
{
    return builder.WithState(x => x.Guid);
}

We’ll wrap a “dictionary of guids to dictionary of property names to list of validation failures”:

public class Errors : Dictionary<Guid, Dictionary<string, List<ValidationFailure>>>
{
    public bool IsValid => Count == 0;
}

Finally, we add an extension method to ValidationResult which maps error messages to a dictionary keyed by the entity guid:

/// <summary>
/// Groups errors by their "CustomState" field then by the
/// "PropertyName" field. Validators should set each error state to
/// entity-under-validation's Guid and use this method to provide
/// grouped error sets back to a client. The client can then use the
/// Guids to map errors back to their corresponding entities when a
/// hierarchy of objects is being validated.
/// </summary>
/// <param name="validationResult"></param>
/// <returns></returns>
public static Errors ToErrors(this ValidationResult validationResult)
{
    var errorsByGuids = validationResult
        .Errors
        .GroupBy(x =>
        {
            if (x.CustomState == null)
            {
                throw new InvalidOperationException(
                    $"Validation rule for '{x.ErrorMessage}' must have " +
                    $".WithGuid() to support ErrorDictionaries.");
            }
            return (Guid)x.CustomState;
        });

    var errors = new Errors();
    foreach (var errorsByGuid in errorsByGuids)
    {
        var errorsByProperties = errorsByGuid
            .GroupBy(x => CanonicalizePropertyName(x.PropertyName))
            .ToDictionary(k => k.Key, v => v.ToList());
        errors.Add(errorsByGuid.Key, errorsByProperties);
    }
    return errors;
}

/// <summary>
/// Strips off anything before a dot in "Parent[42].SomeChildProperty"
/// and converts to lowercase. This is needed so the client can easily
/// look up errors for properties without having to consider
/// capitalisation issues (these are PascalCase in the API and CamelCase
/// in the client) as well as the various ways FluentValidation can
/// name a property - it differs whether we're using child collection
/// validators and a number of other factors.
/// </summary>
private static string CanonicalizePropertyName(string propertyName)
{
    var dotIndex = propertyName.LastIndexOf(".", StringComparison.InvariantCulture);
    if (dotIndex > -1)
    {
        propertyName = propertyName.Substring(
            dotIndex + 1,
            propertyName.Length - dotIndex - 1);
    }

    return propertyName.ToLower();
}

Using the above, our FluentValidation rules would look like:

RuleFor(x => x.Order.Date)
    .NotNull()
    .WithGuid();

RuleFor(x => x.Order.Items)
    .SetCollectionValidator(orderItemsValidator);

And we would return the validation results as an error like so:

orderValidator
    .Validate(command)
    .ToErrors();

We implement a simple binding method that looks like this on the client:

render() {
    const bind = BindConfig({
        context: order,
        errors: CreateOrderStore.errors
    });

    return (
        <div>
            <Form.Input {...bind("date")} />
            <OrderItemsList
                {...bind("items")}
                errors={CreateOrderStore.errors}
            />
        </div>
    );
}

And is implemented like this (in TypeScript):

import * as R from "ramda";

/**
 * Provides data-binding like functionality to any component which implements
 * value and onChange. Example usage:
 * 
 * <SomeInput {...Bind(person, "name")} />
 * 
 * @param context The object whose property should be shown.
 * @param property The property of the object to show.
 */
function Bind(
    context: any,
    property: string,
    onChange?: (e: any, data: any) => void,
    errors?: IErrors,
    errorProperty?: string) {
    if(!(property in context)) {
        throw "Bound property does not exist: " + property;
    }

    // If the context has a guid and we have a dictionary of
    // guids -> properties -> errors then check whether this property is in error
    // and provide that information to the bound component.
    //
    // Consumers can default to checking the bound property name or override
    // and use a different name for error checking. This is useful when the model
    // submitted to the API doesn't exactly match the model being presented (e.g.
    // a property might be called "thing" in a component but submitted to the API
    // as "thingCode", in such cases the errorProperty should be set to "thingCode").
    const checkErrorProperty = (errorProperty != null ? errorProperty : property)
        .toLowerCase();
    const hasErrors =
        context.guid != null &&
        R.path([context.guid, checkErrorProperty], errors) != null;

    return {
        value: context[property],
        onChange: (e: any, data: any) =>  {
            context[property] = data.value;
            if(onChange != null) {
                onChange(e, data);
            }
        },
        error: hasErrors
    };
}

export default Bind;

The Bind method can be partially applied using BindConfig:

import Bind from "./Bind";

interface IBindConfigOptions {
    context: any;
    errors?: IErrors;
}

/**
 * Partially applies a handful of binding options to Bind. These options are
 * usually constant for a given component so this saves us having to specify
 * them for every binding used in a component.
 */
function BindConfig(options: IBindConfigOptions) {
    return (
        property: string,
        onChange?: (e: any, data: any) => void,
        errorProperty?: string
    ) => Bind(options.context, property, onChange, options.errors, errorProperty);
}

export default BindConfig;

The models on the client would (at minimum) have a guid field which is set when the object is instantiated:

export class Order {
    guid: string = GuidService.newGuid();
    date: Moment;
    orderItems: Array<OrderItem> = [];
}

export class OrderItem {
    guid: string = GuidService.newGuid();
}

When errors need to be mapped to a deep hierarchy of objects, nested components should be created for each object in the hierarchy. Pass the entire error object down to each component. The component is then responsible for binding to the appropriate entity and retrieving error messages exactly as in the top-level component.


Back to posts