Search code examples
c#asp.net-corevalidationasp.net-core-webapi

ASP.NET Core Web API custom error message when data type is wrong


I have a controller that accepts data from the request's body

[HttpPatch("update-name")]
public async Task<IActionResult> UpdateName([FromBody] UpdateNameData data)
{
    // ...
}
public class UpdateNameData
{
    [Required(ErrorMessage = "Name is required")]
    public string Name { get; set; } = string.Empty;
}

When I send { "name": 1 }, the API returns this error:

{
  "type": "https://tools.ietf.org/html/rfc9110#section-15.5.1",
  "title": "One or more validation errors occurred.",
  "status": 400,
  "errors": {
    "data": [
      "The data field is required."
    ],
    "$.name": [
      "The JSON value could not be converted to System.String. Path: $.name | LineNumber: 1 | BytePositionInLine: 10."
    ]
  },
  "traceId": "00-4cb41bb3f51e5253c5591ead5fccf96a-ea1c751ebed4be5b-00"
}

How can I make it so that when the user sends a wrong type it returns a custom error message?


Solution

  • Newest UPDATE

    Globally write a generic converter for all the type with wrong type value, you need custom like below:

    Note: C# type is multiple, I just share int,string and double as a sample in this example.

    public class GenericConverter<T> : JsonConverter<T> 
    {
        public override T Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
        {
            if (typeof(T) == typeof(string))
            {
                if (reader.TokenType == JsonTokenType.String)
                {
                    return (T)(object)reader.GetString();
                }
                else
                {
                    throw new JsonException($"String expected, received {reader.TokenType}.");
                        
                }
            }
            else if (typeof(T) == typeof(int))
            {
                try
                {
                    if (reader.TryGetInt32(out int intValue))
                    {
                        return (T)(object)intValue;
                    }
                }
                catch (Exception)
                {
    
                    throw new JsonException($"Integer expected, received {reader.TokenType}.");
    
                }                
            }
            else if (typeof(T) == typeof(double))
            {
                try
                {
                    if (reader.TryGetDouble(out double doubleValue))
                    {
                        return (T)(object)doubleValue;
                    }
                }
                catch (Exception)
                {
    
                    throw new JsonException($"Double expected, received {reader.TokenType}.");
    
                }
            }
            // Add additional type conversions as needed
            else
            {
                throw new NotSupportedException($"Conversion to type {typeToConvert.Name} is not supported.");
            }
            throw new NotSupportedException($"Conversion to type {typeToConvert.Name} is not supported.");
    
        }
    
        public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options)
        {
            writer.WriteStringValue(value.ToString());
        }
    }
    public class GenericConverterFactory : JsonConverterFactory
    {
        public override bool CanConvert(Type typeToConvert)
        {
            // Return true if the typeToConvert is the target type for which you want to apply the converter
            return typeToConvert == typeof(string) || typeToConvert == typeof(int) || typeToConvert == typeof(double);
        }
    
        public override JsonConverter CreateConverter(Type typeToConvert, JsonSerializerOptions options)
        {
            // Create an instance of the generic converter with the appropriate type argument
            Type converterType = typeof(GenericConverter<>).MakeGenericType(typeToConvert);
            return (JsonConverter)Activator.CreateInstance(converterType);
        }
    }
    

    Register the converter like below:

    builder.Services.AddControllers().AddJsonOptions(options =>
    {
        options.JsonSerializerOptions.Converters.Add(new GenericConverterFactory());
    });
    

    "The data field is required."

    You my misunderstand the error message meaning.It means the UpdateNameData data is required.

    For detailed explanation why you get this error message is because you send the wrong data type value and the model binding system cannot bind the value to the model.So the backend parameter will receive null model. And from .NET 6 the non-nullable property must be required, otherwise the ModelState will be invalid. That is why you receive error The data field is required..

    If you add the ? which can the parameter nullable:[FromBody] UpdateNameData? data, then you can see it only receives one error message for cannot convert to string.

    For the Name property, it will not receive the custom required validation error because you send the field but just send the wrong type value.

    If you send empty json:{}, you can get the custom error message you want because you do not send the Name field in json string.

    UPDATE 1

    How can I make it so that when the user sends a wrong type it returns a custom error message?

    ASP.NET Core 2.1 and later version have added the [ApiController] attribute, which automatically handles model validation errors by returning a BadRequestObjectResult with ModelState passed in.

    A simple solution is that you remove the [ApiController] and return your own error message totally:

    if (!ModelState.IsValid)
    {
        return BadRequest(new { ErrorMessage = "Cannot deserialize the string" });
    }
    

    The error message like The JSON value could not be converted to System.String. xxxxx is the build-in error. The default response type for HTTP 400 responses is ValidationProblemDetails class. So, we will create a custom class which inherits ValidationProblemDetails class and define our custom error messages.

    For your current error message is: The JSON value could not be converted to System.String. Path: $.name | LineNumber: 1 | BytePositionInLine: 10.

    public class CustomBadRequest : ValidationProblemDetails
    {
        public CustomBadRequest(ActionContext context)
        {
            ConstructErrorMessages(context);
            Type = context.HttpContext.TraceIdentifier;
        }
    
        private void ConstructErrorMessages(ActionContext context)
        {
           //the build-in error message you get
            var myerror = "The JSON value could not be converted to System.String. Path: $.name | LineNumber: 1 | BytePositionInLine: 10.";
            foreach (var keyModelStatePair in context.ModelState)
            {
                var key = keyModelStatePair.Key;
                var errors = keyModelStatePair.Value.Errors;
                if (errors != null && errors.Count > 0)
                {
                    if (errors.Count == 1)
                    {
                        var errorMessage = GetErrorMessage(errors[0]);
                        if (errorMessage == myerror)
                        {
                            Errors.Add(key, new[] { "The Name must be string" });
                        }
                        else
                        {
                            Errors.Add(key, new[] { errorMessage });
                        }
    
                    }
                    else
                    {
                        var errorMessages = new string[errors.Count];
                        for (var i = 0; i < errors.Count; i++)
                        {
                            errorMessages[i] = GetErrorMessage(errors[i]);
                            if (errorMessages[i] == myerror)
                            {
                                errorMessages[i] = "The Name must be string";
                            }
                        }
    
                        Errors.Add(key, errorMessages);
                    }
                }
            }
        }
    
        string GetErrorMessage(ModelError error)
        {
            return string.IsNullOrEmpty(error.ErrorMessage) ?
                "The input was not valid." :
            error.ErrorMessage;
        }
    }
    

    Program.cs:

    builder.Services.AddControllers().AddXmlSerializerFormatters().ConfigureApiBehaviorOptions(options =>
    {
        options.InvalidModelStateResponseFactory = context =>
        {
            var problems = new CustomBadRequest(context);
    
            return new BadRequestObjectResult(problems);
        };
    });
    

    Another way if you do not want to manully set the error message, you can custom JsonConverter:

    public class CustomStringConverter : JsonConverter<string>
    {
        public override string Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
        {
            try
            {
                if (reader.TokenType == JsonTokenType.String)
                {
                    // If the token is already a string, read and return it
                    return reader.GetString();
                }
                else
                {
                    // Handle other token types or unexpected situations
                    throw new JsonException("Invalid token type. Expected a string.");
                }
            }
            catch (JsonException ex)
            {
                // Custom error message for JSON serialization failure
                throw new JsonException("Error converting value to string");
            }
        }
    
        public override void Write(Utf8JsonWriter writer, string value, JsonSerializerOptions options)
        {
            writer.WriteStartObject();
            writer.WriteString("Name", value);
            writer.WriteEndObject();
        }
    }
    

    Configure the Program.cs:

    builder.Services.AddControllers().AddJsonOptions(options =>
    {
        options.JsonSerializerOptions.Converters.Add(new CustomStringConverter());
    });