【问题标题】:Newtonsoft how to deserialize null to default value again after switching to .Net CoreNewtonsoft 如何在切换到 .Net Core 后再次将 null 反序列化为默认值
【发布时间】:2022-07-12 09:04:47
【问题描述】:

我们将 Asp.Net 应用程序从 .Net Framework 4.7.2 更新到 .Net 5。现在我们在控制器方法中遇到了反序列化 JSON 的问题。在旧版本中,我们使用 Newtonsoft.Json。以前,如果我们在 JSON 中为不可为空的类型(如 int)的属性获得了 null,则反序列化器会采用默认值,或者忽略 null 和错误,并且不会从对象创建中覆盖属性的默认值。但是现在在错误之后整个对象被设置为 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; }
}

和以前一样 将是一个 OrderEffortDto 实例,其中 Cwe = false 并且所有 HoursXXInHours = 0

我们得到什么 是 OrderEffortDto = null

我们已经尝试在新版本中使用 Newtonsoft,但结果相同。我们还配置了SerializerSettings.NullValueHandling = NullValueHandling.Ignore。这适用于该问题,但对于另一个方向,空值也会被忽略,因为在需要空值的地方将 DTO 序列化为 JSON。

有没有办法恢复旧的行为?是的,在前端修复它以将正确的值放入 JSON 是没有问题的,但是我们的应用程序很大,并且要确定所有我们必须纠正的地方,这很容易出错。

更新 1 为可能有同样问题的人提供

我创建了两个简单的测试项目,一个带有 .Net Framework 4.7.2 的 ASP.Net WebApi 和一个带有 .Net 5 的 ASP.Net WebApi,上面带有 JSON 和 DTO 示例。我从 Newtonsoft 获得了两条类似的带有错误的跟踪,并且已经描述了控制器中 DTO 的结果。 .Net 5 中的 System.Text.Json 也给了我整个 DTO 的空值。

适用于 .Net Framework 4.7.2 的 API

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
}

适用于 .Net 5 的 API

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
}

感谢 @dbc 的 cmets。我将在提到的帖子Json.net deserialization null guid case 中尝试使用转换器,但也会记录出现的情况以修复根本原因。

更新 2

我稍微改动了转换器并使用了“SwaggerGen.TypeExtensions.GetDefaultValue()”。所以我能够删除泛型并为所有不可为空的类型使用一个转换器。

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

【问题讨论】:

  • @das.fliagsi 你能在可以为 null 的属性顶部尝试 [JsonProperty] 看看它是否正确反序列化
  • 不幸的是不起作用。它可以与 [JsonProperty(NullValueHandling = NullValueHandling.Ignore)] 一起使用,但我又遇到了另一个方向的问题。
  • 在 .NET 5 中,您是否已从 Json.NET 切换到 System.Text.Json?顺便说一句,我认为 Json.NET 不会自动将 null 值反序列化为不可为空的值类型的默认值;你需要一个转换器。参见例如Json.net deserialization null guid case.
  • @dbc 是的,我也尝试使用 System.Text.Json。在那里,我还得到了整个对象的空对象。 NullValueHandling 和 DefaultValueHandling 的设置非常相似。 Json.Net 的一个好处是 TraceWriter。正因为如此,我同意你可能不会发生转变。因为此跟踪在反序列化非可空类型的空值时显示错误。但是这个错误被忽略并且属性被跳过。因此,由于 OrderEffortDto 对象的正常创建,不可为空的属性已经填充了默认值。
  • @das.flaigsi - 如果 Json.NET 总是产生错误,但在过去,asp.net 会在基于每个属性的模型绑定期间忽略它们,但现在它们会导致模型绑定完全失败,而不是试图恢复旧的行为,我会修复错误。将 NullToDefaultConverter<T>Json.net deserialization null guid case 添加到所有必要的 TJsonSerializerSettings.Converters 应该可以完成这项工作。事实上,我可能会建议将其作为副本关闭,同意吗?

标签: asp.net json asp.net-core json.net


【解决方案1】:

只要让属性可以为空

public class OrderEffortDto
{
    .........
    public bool? Cwe { 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; }
   
}

或者你可以添加一个构造函数而不是使为空。只能在构造函数中包含反序列化过程中需要更改的属性

[Newtonsoft.Json.JsonConstructor]
     public OrderEffortDto(
     bool? cwe ,
     decimal? effortInHours ,
     decimal? hours25InHours ,
     decimal? hours50InHours ,
     decimal? hours100InHours ,
     decimal? hours150InHours )
    {
        Cwe = cwe==null?false: (bool) Cwe;
        EffortInHours = effortInHours==null? 0: (decimal) effortInHours;
       .... and so on
    }
    

【讨论】:

  • 不,因为它对于业务逻辑来说是不可为空的,至少对于所有的逻辑来说都不是。
  • @das.flaigsi 查看我的更新答案
  • Uff..好的,这将工作。但几乎就像在前端修复它一样。很难找到所有的地方,而且容易出错。但我会记住的。谢谢。
【解决方案2】:

这是一个很好的问题,我最近在将大型(受错误困扰的)遗留代码库从 .NET Framework 迁移到 .NET Core 时也遇到了这个问题。

OP 的更新非常有帮助,但我想分享一个稍微简化且性能更高的解决方案,它消除了对 SwaggerGen.TypeExtensions.GetDefaultValue() 的依赖以及没有通用依赖;因此它可以作为转换器全局应用:

private static readonly NullToDefaultConverter _jsonLegacyCompatibleNullValueConverter = new();

   . . .

var model = JsonConvert.DeserializeObject(content, modelType, _jsonLegacyCompatibleNullValueConverter);

当 Json.NET 反序列化当前现有值或默认值时,通过 object existingValue 输入参数提供,该参数将具有属性的默认值或由属性初始化程序设置的初始值。在这两种情况下,我们都可能希望保留该值,因此可以只返回它,从而提供与旧版 .NET Framework Web API 行为更兼容的行为(如 OP 的帖子中所述)。

还请注意,我们只需要处理 无法分配 null 值的属性,因此可以对 Type 进行更高效的检查(改编自优雅的 @987654321 @)。

/// <summary>
/// BBernard
/// This is a Json Converter that restores legacy ASP.Net MVC compatible behavior for handling values that cannot be assigned Null.
/// Historically (before .NET Core) Json values of null would result in errors when setting them into values that cannot be assigned null,
///     however these errors treated as warnings and were skipped, leaving the original default values set on the Model. Now in 
///     Asp .NET Core, these failures result in Exceptions being thrown.
/// While previously the result was that these fields were simply left their default values; either the default of the Value type 
///     or the default set in the Property Initializer.
/// Therefore this Json Converter restores this behavior by assigning the Default value safely without any errors being thrown.
/// </summary>
public class NullToDefaultConverter : JsonConverter
{
    public static bool CanTypeBeAssignedNull(Type type)
        => !type.IsValueType || (Nullable.GetUnderlyingType(type) != null);

    //BBernard - ONLY Handle Fields that would fail a Null assignment and requires resolving of a non-null existing/default value for the Type!
    public override bool CanConvert(Type objectType) => !CanTypeBeAssignedNull(objectType);

    public override object ReadJson(JsonReader reader, Type objectType, object existingOrDefaultValue, JsonSerializer serializer)
    {
        var token = JToken.Load(reader);
        return token.Type == JTokenType.Null ? existingOrDefaultValue : token.ToObject(objectType);
    }

    // Return false; we want normal Json.NET behavior when serializing...
    public override bool CanWrite => false;

    public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) => throw new NotImplementedException();
}


enter code here

【讨论】:

    猜你喜欢
    • 2023-03-18
    • 2021-08-07
    • 2014-03-03
    • 1970-01-01
    • 2020-05-31
    • 1970-01-01
    • 2022-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多