We updated our Asp.Net application from .Net Framework 4.7.2 to .Net 5. Now we have problems with deserialization JSON in the controller methods. In old version we used Newtonsoft.Json. Former if we got a null for a property in JSON for a non nullable type like int the deserializer took the default value, or ignored the null and the error and did not overwrite the property’s default value from object creation. But now after the error the whole object is set to null.
{
"effortType": "1",
"cwe": null,
"distanceInKilometers": null,
"effortDate": "2022-03-22T14:45:00+01:00",
"effortInHours": 1.0,
"hours25InHours": null,
"hours50InHours": null,
"hours100InHours": null,
"hours150InHours": null,
"orderNumber": "006001780872",
"withCosts": false,
"isNew": true,
"isEdited": false,
"isDeleted": false
}
public class OrderEffortDto
{
public string EffortType { get; set; }
public bool Cwe { get; set; }
public int? DistanceInKilometers { get; set; }
public DateTimeOffset? EffortDate { get; set; }
public decimal EffortInHours { get; set; }
public decimal Hours25InHours { get; set; }
public decimal Hours50InHours { get; set; }
public decimal Hours100InHours { get; set; }
public decimal Hours150InHours { get; set; }
public string OperationNumber { get; set; }
public bool IsNew { get; set; }
public bool IsEdited { get; set; }
public bool IsDeleted { get; set; }
}
Expected like before
would be a OrderEffortDto instance with Cwe = false and all HoursXXInHours = 0
What we get
is OrderEffortDto = null
We already tried to use Newtonsoft also in new version, but with the same result. We also configured SerializerSettings.NullValueHandling = NullValueHandling.Ignore
. This works for that problem, but than the null values are also ignored for the other direction, for serialization of DTOs into JSON, where the nulls are needed.
Is there a way to get back the old behavior? Right, it would be no problem to fix that in front end to get the right values into the JSON, but our application is big and to determine all the places, where we have to correct that is fault-prone.
Update 1 for those who may have the same problem
I created two simple test projects one ASP.Net WebApi with .Net Framework 4.7.2 and one ASP.Net WebApi with .Net 5, with above JSON and DTO example. I got two similar traces with errors from Newtonsoft and already described results for the DTO in the Controllers. Also the System.Text.Json in .Net 5 gave me a null for the whole DTO.
For API with .Net Framework 4.7.2
2022-03-24T10:50:05.368 Info Started deserializing WebApplication1NetFramework.Data.OrderEffortDto. Path 'effortType', line 2, position 16.
2022-03-24T10:50:05.388 Error Error deserializing WebApplication1NetFramework.Data.OrderEffortDto. Error converting value {null} to type 'System.Boolean'. Path 'cwe', line 3, position 14.
2022-03-24T10:50:05.403 Error Error deserializing WebApplication1NetFramework.Data.OrderEffortDto. Error converting value {null} to type 'System.Decimal'. Path 'hours25InHours', line 7, position 25.
2022-03-24T10:50:05.403 Error Error deserializing WebApplication1NetFramework.Data.OrderEffortDto. Error converting value {null} to type 'System.Decimal'. Path 'hours50InHours', line 8, position 25.
2022-03-24T10:50:05.403 Error Error deserializing WebApplication1NetFramework.Data.OrderEffortDto. Error converting value {null} to type 'System.Decimal'. Path 'hours100InHours', line 9, position 26.
2022-03-24T10:50:05.404 Error Error deserializing WebApplication1NetFramework.Data.OrderEffortDto. Error converting value {null} to type 'System.Decimal'. Path 'hours150InHours', line 10, position 26.
2022-03-24T10:50:05.404 Verbose Could not find member 'orderNumber' on WebApplication1NetFramework.Data.OrderEffortDto. Path 'orderNumber', line 11, position 17.
2022-03-24T10:50:05.405 Verbose Could not find member 'withCosts' on WebApplication1NetFramework.Data.OrderEffortDto. Path 'withCosts', line 12, position 15.
2022-03-24T10:50:05.407 Info Finished deserializing WebApplication1NetFramework.Data.OrderEffortDto. Path '', line 16, position 1.
2022-03-24T10:50:05.407 Verbose Deserialized JSON:
{
"effortType": "1",
"cwe": null,
"distanceInKilometers": null,
"effortDate": "2022-03-22T14:45:00+01:00",
"effortInHours": 1.0,
"hours25InHours": null,
"hours50InHours": null,
"hours100InHours": null,
"hours150InHours": null,
"orderNumber": "006001780872",
"withCosts": false,
"isNew": true,
"isEdited": false,
"isDeleted": false
}
For API with .Net 5
2022-03-24T10:48:19.162 Info Started deserializing WebApplication1NetCore.Data.OrderEffortDto. Path 'effortType', line 2, position 16.
2022-03-24T10:48:19.180 Error Error deserializing WebApplication1NetCore.Data.OrderEffortDto. Error converting value {null} to type 'System.Boolean'. Path 'cwe', line 3, position 14.
2022-03-24T10:48:19.196 Error Error deserializing WebApplication1NetCore.Data.OrderEffortDto. Error converting value {null} to type 'System.Decimal'. Path 'hours25InHours', line 7, position 25.
2022-03-24T10:48:19.196 Error Error deserializing WebApplication1NetCore.Data.OrderEffortDto. Error converting value {null} to type 'System.Decimal'. Path 'hours50InHours', line 8, position 25.
2022-03-24T10:48:19.197 Error Error deserializing WebApplication1NetCore.Data.OrderEffortDto. Error converting value {null} to type 'System.Decimal'. Path 'hours100InHours', line 9, position 26.
2022-03-24T10:48:19.197 Error Error deserializing WebApplication1NetCore.Data.OrderEffortDto. Error converting value {null} to type 'System.Decimal'. Path 'hours150InHours', line 10, position 26.
2022-03-24T10:48:19.197 Verbose Could not find member 'orderNumber' on WebApplication1NetCore.Data.OrderEffortDto. Path 'orderNumber', line 11, position 17.
2022-03-24T10:48:19.197 Verbose Could not find member 'withCosts' on WebApplication1NetCore.Data.OrderEffortDto. Path 'withCosts', line 12, position 15.
2022-03-24T10:48:19.199 Info Finished deserializing WebApplication1NetCore.Data.OrderEffortDto. Path '', line 16, position 1.
2022-03-24T10:48:19.200 Verbose Deserialized JSON:
{
"effortType": "1",
"cwe": null,
"distanceInKilometers": null,
"effortDate": "2022-03-22T14:45:00+01:00",
"effortInHours": 1.0,
"hours25InHours": null,
"hours50InHours": null,
"hours100InHours": null,
"hours150InHours": null,
"orderNumber": "006001780872",
"withCosts": false,
"isNew": true,
"isEdited": false,
"isDeleted": false
}
Thanks go out to @dbc for the comments. I will try it with the convertor in the mentioned post Json.net deserialization null guid case, but also will log the occurrences to fix the root cause.
Update 2
I altered the converter a little and used the "SwaggerGen.TypeExtensions.GetDefaultValue()". So I was able to remove the generic and use one converter for all non-nullable types.
public class NullToDefaultConverter : JsonConverter
{
public override bool CanConvert(Type objectType)
{
var defaultValue = objectType.GetDefaultValue();
return defaultValue != null;
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
var token = JToken.Load(reader);
if (token.Type == JTokenType.Null)
// here I will add a logger to get all faulty calls
return objectType.GetDefaultValue();
return token.ToObject(objectType); // Deserialize using default serializer
}
// Return false I don't want default values to be written as null
public override bool CanWrite => false;
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
throw new NotImplementedException();
}
}
2
Answers
just make properties nullable
or you can add a constructor instead of making nullable. You can only include in the constructor the properties that needs to be changed during deserialization
This is a great question and one that I also encountered recently when migrating a large (error plagued) legacy code base from .NET Framework to .NET Core.
The OP’s updates are very helpful but I wanted to share a slightly simplified and higher performing solution that eliminates the dependency on
SwaggerGen.TypeExtensions.GetDefaultValue()
as well as no Generic dependency; so it can be applied globally as a Converter:When Json.NET is deserializing the current existing or default value is provided via the
object existingValue
input parameter which will have the default value for a property or the initial value set by the property initializer. In both cases we likely want to preserve that value and therefore can just return it thereby providing behavior that is more compatible with the legacy .NET Framework Web API behavior (as described in the OP’s post).Notice also that we only have to handle the properties that can not be assigned a
null
value, so a more performant check can be done there on the Type (adapted from the elegant stack overflow answer here).And to further expand on how to restore legacy compatible behavior globally, we implemented a custom Input Formatter as follows:
Which is configured for all model binding to use via:
EDIT – Optimized version now shared on Nuget:
Since originally sharing this solution I realized that there were several performance impacts and a couple other compatibility items, such as error handling, that needed to be addressed. I’ve now shared an optimized version , on GitHub & Nuget, that implements a custom
JsonBufferedHttpRequestReader
andCharArrayPool
vs serializing to string with unnecessary allocations. It also enables legacy compatibility for error handling by pushing all errors into theModelState
and preventing those exceptions from being thrown.This full version is available on Nuget and located here:
https://github.com/cajuncoding/AspNetCoreMigrationShims
And can be configured easily just as Newtonsoft is via: