Custom ApiController Model Binding type conversion error

iKingNinja 60 Reputation points
2024-04-24T10:30:29.13+00:00

According to the Model Binding documentation:

In an API controller that has the [ApiController] attribute, invalid model state results in an automatic HTTP 400 response.

So when I send an invalid type in the request body (e.g. int instead of string), the response is:

{
  "type": "https://tools.ietf.org/html/rfc9110#section-15.5.1",
  "title": "One or more validation errors occurred.",
  "status": 400,
  "errors": {
    "$.about": [
      "The JSON value could not be converted to System.String. Path: $.about | LineNumber: 1 | BytePositionInLine: 10."
    ]
  },
  "traceId": "00-70adaedd9c4a12800ff57a680cd1e729-40357df5d5d750bf-00"
}

How can I customize the "The JSON value could not be converted to..." error message to something like "about must be of type string"?

ASP.NET Core
ASP.NET Core
A set of technologies in the .NET Framework for building web applications and XML web services.
4,189 questions
C#
C#
An object-oriented and type-safe programming language that has its roots in the C family of languages and includes support for component-oriented programming.
10,279 questions
0 comments No comments
{count} votes

Accepted answer
  1. Ping Ni-MSFT 2,250 Reputation points Microsoft Vendor
    2024-04-25T02:34:52.91+00:00

    Hi @iKingNinja,

    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());
    });
    

    How can I customize the "The JSON value could not be converted to..." error message to something like "about must be of type string"?

    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());
        });
    

    You can also try to custom the response model like what Zhi Lv's answer here.

    If the answer is the right solution, please click "Accept Answer" and kindly upvote it. If you have extra questions about this answer, please click "Comment".
    Note: Please follow the steps in our documentation to enable e-mail notifications if you want to receive the related email notification for this thread.
    Best regards,
    Rena

    1 person found this answer helpful.

1 additional answer

Sort by: Most helpful
  1. Bruce (SqlWork.com) 56,846 Reputation points
    2024-04-24T15:59:53.52+00:00
    0 comments No comments