string.empty converted to null when passing JSON object to MVC Controller

26,269

Solution 1

This is a MVC feature which binds empty strings to nulls.

This logic is controlled with the ModelMetadata.ConvertEmptyStringToNull property which is used by the DefaultModelBinder.

You can set the ConvertEmptyStringToNull with the DisplayFormat attribute

public class OrderDetailsModel
{
    [DisplayFormat(ConvertEmptyStringToNull = false)]
    public string Comment { get; set; }

    //...
}

However if you don't want to annotate all the properties you can create a custom model binder where you set it to false:

public class EmptyStringModelBinder : DefaultModelBinder 
{
    public override object BindModel(ControllerContext controllerContext,
                                     ModelBindingContext bindingContext)
    {
        bindingContext.ModelMetadata.ConvertEmptyStringToNull = false;
        Binders = new ModelBinderDictionary() { DefaultBinder = this };
        return base.BindModel(controllerContext, bindingContext);
    }
}

And you can use the ModelBinderAttribute in your action:

public ActionResult SaveOrderDetails([ModelBinder(typeof(EmptyStringModelBinder))] 
       OrderDetailsModel orderDetailsModel)
{
}

Or you can set it as the Default ModelBinder globally in your Global.asax:

ModelBinders.Binders.DefaultBinder = new EmptyStringModelBinder();

You can read more about this feature here.

Solution 2

Instead of creating a ModelBinder which modifies the ModelMetadata as some answers suggested, a cleaner alternative is to provide a custom ModelMetadataProvider.

public class EmptyStringDataAnnotationsModelMetadataProvider : System.Web.Mvc.DataAnnotationsModelMetadataProvider 
{
    protected override ModelMetadata CreateMetadata(IEnumerable<Attribute> attributes, Type containerType, Func<object> modelAccessor, Type modelType, string propertyName)
    {
        var modelMetadata = base.CreateMetadata(attributes, containerType, modelAccessor, modelType, propertyName);
        modelMetadata.ConvertEmptyStringToNull = false;
        return modelMetadata;
    }
}

Then in Application_Start()

ModelMetadataProviders.Current = new EmptyStringDataAnnotationsModelMetadataProvider();

Solution 3

The accepted answer did not work for me using MVC4. However, the following workaround does and I thought it would help others.

public class CustomModelBinder : DefaultModelBinder
{
    public bool ConvertEmptyStringToNull { get; set; }

    public CustomModelBinder ()
    {
    }

    public CustomModelBinder (bool convertEmptyStringToNull)
    {
        this.ConvertEmptyStringToNull = convertEmptyStringToNull;
    }

    protected override bool OnModelUpdating(ControllerContext controllerContext, ModelBindingContext bindingContext)
    {
        // this little bit is required to override the ConvertEmptyStringToNull functionality that we do not want!

        foreach (string propertyKey in bindingContext.PropertyMetadata.Keys)
        {
            if(bindingContext.PropertyMetadata[propertyKey] != null)
                    bindingContext.PropertyMetadata[propertyKey].ConvertEmptyStringToNull = this.ConvertEmptyStringToNull;
        }
        return base.OnModelUpdating(controllerContext, bindingContext);
    }


}

This will fix the issue under MVC4+. It would seem that bindingContext.ModelMetadata.ConvertEmptyStringToNull is completely ignored, and this is because the setting exists in the PropertyMetadata object for each property being bound. PropertyMetadata is recreated in BindProperty() so if you set it before that method call it will get overwritten unless it exists as an attribute on the property of your object being bound (such as [DisplayFormat(ConvertEmptyStringToNull=false)]). No one wants to do this on every property as that's silly.

Share:
26,269
Sean Anderson
Author by

Sean Anderson

Sr. Software Developer and blockchain enthusiast w/ ~8 years of full-stack experience in JavaScript and C#. Extensive open-source contributions and a part of the MarionetteJS organization. Currently full-time day trader.

Updated on December 02, 2020

Comments

  • Sean Anderson
    Sean Anderson over 3 years

    I'm passing an object from client to server. Properties of the object which are represented as string.empty are being converted to null during this process. I was wondering how to prevent this when the objects type supports string.empty.

    enter image description here

    console.log("DataToPost:", dataToPost);
    
    $.ajax({
        type: "POST",
        contentType: 'application/json'
        url: "../../csweb/Orders/SaveOrderDetails/",
        data: dataToPost,
        success: function (result) {
            console.log(result);
        },
        error: function (e) {
            console.error(e);
        }
    });
    

    enter image description here

    My model includes nullable DateTime objects. I cannot force all nulls to string.empty on the server.

    I am using AutoMapper, so I would prefer not to have to inspect properties individually on the server.

  • Sean Anderson
    Sean Anderson over 11 years
    Thanks for your response. If you'll view my second screenshot, you'll see that my watch window is hovering over the paramater passed into SaveOrderDetails. AutoMapper has not come into play just yet, but perhaps AutoMapper could solve the issue. I'll have a play with it.
  • Sean Anderson
    Sean Anderson over 11 years
    This is clearly the correct answer to the problem. Thank you. Update: Works as advertised, too!
  • Anders
    Anders about 11 years
    If I set the ModelBinderAttribute-attribute for just a single parameter as described, the solution above does not work. It appears the standard DefaultModelBinder is used when binding the individual model properties, which makes them null anyway. My fix for this was to add the following code to the beginning of the overridden BindModel()-method: Binders = new ModelBinderDictionary() { DefaultBinder = this };
  • YMC
    YMC about 11 years
    Confirmed: the example above does not work applying attribute in action
  • nemesv
    nemesv about 11 years
    @Anders thanks for the fix. I've update my post with the information.
  • mac10688
    mac10688 about 11 years
    using System.ComponentModel.DataAnnotations;
  • Michael Brown
    Michael Brown about 10 years
    Please see my recommended solution below, if this is not working for you (it did not work for me, as I would have liked).
  • XDS
    XDS about 7 years
    I'm facing a similar problem with arrays. Json strings like "{ foo: [] }" result in foo=null when it comes to the parameters of mvc-actions. Is there any way to tweak the deserialization of the default mvc json-deserializer so that it will convert the given json-string to an actual empty array (aka not null)?
  • Adam Venezia
    Adam Venezia almost 7 years
    In MVC 5.2.3, ModelMetadataProviders.Current returns an instance of CachedDataAnnotationsModelMetadataProvider by default. The CachedDataAnnotationsModelMetadataProvider seals the CreateMetaData method so you can't override it like you are suggesting here. I'm guessing the cached provider is more performant, so I'm not sure if this answer is a good idea since it does not use the cached provider (and can't anyway).