6

I have a model where I am using DataAnnotations to perform validation, such as

public class OrderDTO
{
    [Required]
    public int Id { get; set; }

    [Required]
    public Decimal Amount { get; set; }
}

Then I am checking the ModelState in each request to make sure that the JSON is valid.

However, I am having trouble for number properties such as Amount above. Even though it is set as [Required], if it's not included in the JSON it will skip the ModelState validation because it is automatically defaulted to 0 instead of null, so the model will seem valid even though it isn't.

An easy way to 'fix' this is to set all the number properties as nullable (int?, Decimal?). If I do this, the defaulting to 0 doesn't happen, but I don't like this as a definitive solution as I need to change my model.

Is there a way to set the properties to null if they are not part of the JSON?

erictrigo
  • 989
  • 2
  • 13
  • 41

4 Answers4

7

Because Decimal is a non-nullable type so you cannot do that. You need Decimal? to bind null value.

Tseng
  • 61,549
  • 15
  • 193
  • 205
tdat00
  • 810
  • 1
  • 8
  • 11
3

You have to use a nullable type. Since a non-nullable value, as you know, cannot be null then it will use 0 as a default value and therefore appear to have a value and always pass the validation.

As you have said it has to be null for the validation to work and therefore be nullable. Another option could be to write your own validation attribute but this could then cause a problem as you would most likely be saying if is null or 0 then not valid, a big issue when you want to have 0 as an accepted value because you then need another way of deciding when 0 is and isn't valid.

Example for custom validation, not specific to this case. Web API custom validation to check string against list of approved values

A further option could be to add another property that is nullable and provides the value to the non-nullable property. Again, this could cause issues with the 0 value. Here is an example with the Id property, your json will now need to send NullableId rather than Id.

public class OrderDTO
{
    //Nullable property for json and validation 
    [Required]
    public int? NullableId {
        get {
            return Id == 0 ? null : Id; //This will always return null if Id is 0, this can be a problem
        }
        set {
            Id = value ?? 0; //This means Id is 0 when this is null, another problem
        }
    }

    //This can be used as before at any level between API and the database
    public int Id { get; set; }
}

As you say another option is to change the model to nullable values through the whole stack.

Finally you could look at having an external model coming into the api with nullable properties and then map it to the current model, either manually or using something like AutoMapper.

Community
  • 1
  • 1
Dhunt
  • 1,584
  • 9
  • 22
1

I agree with others that Decimal being a non Nullable type cannot be assigned with a null value. Moreover, Required attribute checks for only null, empty string and whitespaces. So for your specific requirement you can use CustomValidationAttribute and you can create a custom Validation Type to do the "0" checking on Decimal properties.

Martin
  • 644
  • 5
  • 11
1

There is no way for an int or Decimal to be null. That is why the nullables where created.

You have several options [Edit: I just realized that you are asking for Web-API specifically and in this case I believe the custom binder option would be more complex from the code I posted.]:

  • Make the fields nullable in your DTO
  • Create a ViewModel with nullable types, add the required validation attributes on the view model and map this ViewModel to your DTO (maybe using automapper or something similar).
  • Manually validate the request (bad and error prone thing to do)

    public ActionResult MyAction(OrderDTO order)
    {
       // Validate your fields against your possible sources (Request.Form,QueryString, etc)
       if(HttpContext.Current.Request.Form["Ammount"] == null)
       {
           throw new YourCustomExceptionForValidationErrors("Ammount was not sent");
       }
    
       // Do your stuff
    }
    

  • Create a custom binder and do the validation there:

    public class OrderModelBinder : DefaultModelBinder 
    {
       protected override bool OnPropertyValidating(ControllerContext controllerContext, ModelBindingContext bindingContext,
    PropertyDescriptor propertyDescriptor, object value)
       {
           if ((propertyDescriptor.PropertyType == typeof(DateTime) && value == null) ||
              (propertyDescriptor.PropertyType == typeof(int) && value == null) ||
              (propertyDescriptor.PropertyType == typeof(decimal) && value == null) ||
              (propertyDescriptor.PropertyType == typeof(bool) && value == null))
           {
               var modelName = string.IsNullOrEmpty(bindingContext.ModelName) ? "" : bindingContext.ModelName + ".";
               var name = modelName + propertyDescriptor.Name;
               bindingContext.ModelState.AddModelError(name, General.RequiredField);
            }
    
        return base.OnPropertyValidating(controllerContext, bindingContext, propertyDescriptor, value);
        }
    }
    

    And register your binder to your model using one of the techniques described in the following answer: https://stackoverflow.com/a/13749124/149885 For example:

    [ModelBinder(typeof(OrderBinder))]
    public class OrderDTO
    {
        [Required]
        public int Id { get; set; }
    
        [Required]
        public Decimal Amount { get; set; }
    }
    
Community
  • 1
  • 1
Edgar Hernandez
  • 4,020
  • 1
  • 24
  • 27