【问题标题】:How to deal with mapping intentional NULL values passed through JSON?如何处理通过 JSON 传递的映射有意的 NULL 值?
【发布时间】:2020-11-17 16:32:13
【问题描述】:

总结

假设我正在设计一个 API 来执行 SQL Server SELECT 查询。我有几个必需参数和一些可选参数。但是,如果在负载中发送 null 值,这是 正确 并且 有意,但我无法分辨当前的差异我反序列化 JSON 的方式。我要反序列化的属性的值默认为 null。我的问题是我无法判断它是否被填写,因为没有标记。

示例

以我目前的方式(下面的示例),我无法区分用户是否想要:

  1. 其实是找一个空值
  2. 如果用户对查找特定字段不感兴趣,则从序列化中删除空值
using Newtonsoft.Json;

namespace test
{
    public class SomeClass
    {
        public string RequiredProperty {get;set;}
        public string RequiredProperty2 {get;set;}
        public string OptionalProperty3 {get;set;}

        public SomeClass(){}

    }

    class Program
    {
        static void Main(string[] args)
        {   
            // Example JSON payloads
            string JsonExample1 = @"
            {
                ""RequiredProperty"":""search"",
                ""RequiredProperty2"":""this"",
                ""OptionalProperty3"":null
            }";

            string JsonExample2 = @"
            {
                ""RequiredProperty"":""search"",
                ""RequiredProperty2"":""this""
            }";
            
            // Deserializing JsonExample1
            SomeClass sc1 = JsonConvert.DeserializeObject<SomeClass>(JsonExample1);

            // Deserializing JsonExample2 - identical to Example1 even though the INTENTION is completely different
            SomeClass sc2 = JsonConvert.DeserializeObject<SomeClass>(JsonExample2);

            // Now, using the model, I am unable to tell what the user's intentions actually were.
        }
    }
}   

问题

1. 我解决这个问题的方法甚至正确吗?
  • 我尝试使用 Attributes 创建和“IsSet”标签,但发现它没有用,因为它附加到类型并且无法在运行时更改。
  • 我是否应该将 json 的默认值保留为某种特殊字符串(即“##notset##”)?
  • 这种事情有最佳实践吗?大家能想到什么例子吗?
2. 通过 JSON 传递可选参数是否也合适?我选择对所有事情都使用 JSON 有效负载,因为我工作的公司有这些疯狂的有效负载需要传递。

【问题讨论】:

标签: json .net-core json.net azure-functions api-design


【解决方案1】:

我想出的解决方案。我在使用 JsonConverter 时遇到了很多问题,所以我选择使用 ContractResolver。似乎这通常只是我们目前如何使用 API 的限制。实际上有很多关于这个的讨论。真的没有办法区分它的默认值和已经设置的东西。我只是选择创建一些字符串填充值来指示用户已在 JSON 中发送了该值。这使 JSON 与传递时发送的完全相同。但是,映射的结果完全不同,开发人员可以看出现在实际上还没有设置。

不幸的是,我不得不提出 2 个合约解析器,因为它们本身并不处理对象的 instance,而且我对当前实例的条件序列化感兴趣。我最终不得不实例化我的自定义合同解析器并传递我感兴趣的对象的实例。

如果有人知道如何更好地做到这一点,请告诉我!总的来说,我花了很多时间浏览 GitHub 并将不同的堆栈溢出帖子拼凑在一起。

代码: 解串器

// PreInstalled Packages
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;

// From NuGet - Default
using Newtonsoft.Json;
using Newtonsoft.Json.Serialization;

    public class CrudDeserializer: DefaultContractResolver
    {
        // Designated respresentation for a null value passed through JSON, its default is "JsonNull"
        private string NullRepresentation;

        protected override IList<JsonProperty> CreateProperties(Type type, MemberSerialization memberSerialization)
        {
            return type.GetProperties()
                    .Select(p=>{
                        var jp = base.CreateProperty(p, memberSerialization);
                        jp.ValueProvider = new NullToUniqueStringValueProvider(p, this.NullRepresentation);
                        return jp;
                    }).ToList();
        }

        public CrudDeserializer(string nullRepresentation = "JsonNull")
        {
            if(nullRepresentation == null)
            {
                throw new Exception ("nullRepresentation cannot be NULL. It kind of defeats the purpose");
            }

            this.NullRepresentation = nullRepresentation;
        }
    }


// Second class
    public class NullToUniqueStringValueProvider : IValueProvider
    {
        private PropertyInfo MemberInfo;
        private string NullRepresentation;

        public NullToUniqueStringValueProvider(PropertyInfo memberInfo, string nullRepresentation)
        {
            this.MemberInfo = memberInfo;
            this.NullRepresentation = nullRepresentation;
        }

        public object GetValue(object target)
        {
            throw new Exception("This class is not used for serialization");
        }

        public void SetValue(object target, object value)
        {
            if ((string)value == null)
            {
                MemberInfo.SetValue(target, this.NullRepresentation);
            }
            else
            {
                MemberInfo.SetValue(target, value);
            }
        }
    }

序列化器

// PreInstalled Packages
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;

// From NuGet - Default
using Newtonsoft.Json;
using Newtonsoft.Json.Serialization;

    public class CrudSerializer<T>: DefaultContractResolver
    {
        private string NullRepresentation;
        private T InstantiatedObject;


        protected override IList<JsonProperty> CreateProperties(Type type, MemberSerialization memberSerialization)
        {
            return type.GetProperties()
                    .Select(p=>{
                        var jp = this.CreateProperty(p, memberSerialization);
                        jp.ValueProvider = new UniqueStringToNull(p, this.NullRepresentation);
                        return jp;
                    }).ToList();
        }

        protected override JsonProperty CreateProperty(MemberInfo member, MemberSerialization memberSerialization)
        {
            JsonProperty property = base.CreateProperty(member, memberSerialization);
            PropertyInfo pi = member as PropertyInfo;
            string value =  (string)pi.GetValue(this.InstantiatedObject);
            if (value == null)
            {
                property.ShouldSerialize =
                    instance =>
                    {
                        return false;
                    };
            }

            return property;
    }

        public CrudSerializer(T someInstance, string nullRepresentation = "JsonNull")
        {
            if(nullRepresentation == null)
            {
                throw new Exception ("nullRepresentation cannot be NULL. It kind of defeats the purpose");
            }

            this.NullRepresentation = nullRepresentation;
            this.InstantiatedObject = someInstance;
        }
    }

    public class UniqueStringToNull : IValueProvider
    {
        private PropertyInfo MemberInfo;
        private string NullRepresentation;

        public UniqueStringToNull(PropertyInfo memberInfo, string nullRepresentation)
        {
            this.MemberInfo = memberInfo;
            this.NullRepresentation = nullRepresentation;
        }

        public object GetValue(object target)
        {
            object result =  MemberInfo.GetValue(target);
            if (MemberInfo.PropertyType == typeof(string) && (string)result == this.NullRepresentation)
            {
                result = null;
            }
            return result;

        }

        public void SetValue(object target, object value)
        {
            throw new Exception ("This ContractResolver cannot be used for deserialization");
        }
    }

例子:

    public class SomeClass
    {
        
        public string RequiredProperty {get;set;}
        public string RequiredProperty2 {get;set;}

        public string OptionalProperty3 {get;set;}
        public string OptionalProperty4 {get;set;}

        public SomeClass(){}

    }
    class Program
    {
        static void Main(string[] args)
        {   
            // Example JSON payloads
            string JsonExample1 = @"
            {""RequiredProperty"":""search"",""RequiredProperty2"":""this"",""OptionalProperty3"": null}";

            string JsonExample2 = @"
            {""RequiredProperty"":""search"",""RequiredProperty2"":""this""}";
            
            JsonSerializerSettings deserializationSettings = new JsonSerializerSettings { 
                ContractResolver = new CrudDeserializer()
                };
                

            // Deserializing/Serializing JsonExample1
            SomeClass sc1 = JsonConvert.DeserializeObject<SomeClass>(JsonExample1, deserializationSettings );

            // Passing over current instance of object to ContractResolver
            JsonSerializerSettings serializationSettings1 = new JsonSerializerSettings { 
                ContractResolver = new CrudSerializer<SomeClass>(sc1)
                };
            string json1 =  JsonConvert.SerializeObject(sc1, Formatting.None, serializationSettings1);


            // Deserializing/Serializing JsonExample2 
            SomeClass sc2 = JsonConvert.DeserializeObject<SomeClass>(JsonExample2, deserializationSettings);

            // Passing over current instance of object to ContractResolver
            JsonSerializerSettings serializationSettings2 = new JsonSerializerSettings { 
                ContractResolver = new CrudSerializer<SomeClass>(sc2)
                };

            string json2 =  JsonConvert.SerializeObject(sc2,  Formatting.None, serializationSettings2);

            // Done, JSON is the exact same no matter how many times I deserialize.
            // However, in the background, I am able to tell now if a NULL value was sent!
        }

    }

【讨论】:

    【解决方案2】:

    安迪提供的评论是正确的,我也不知道你在问题中提到的两者之间的区别。我认为 OptionalProperty3 的值为 null 与 OptionalProperty3 的值相同。

    如果你还想区分它们,这里我可以提供一个解决方法供你参考。将JsonExample1中的null替换为“null”字符串,请参考我下面的代码:

    【讨论】:

    • 啊,我明白了。我遇到的问题是当我反序列化时,无论是否发送 null 都会产生一个 null 值。在某些情况下,空值是有意的,会被某些东西使用。问题是,默认情况下,无论是否在 JSON 中提供,设置的任何属性都将为 null。这有意义吗?
    • 嗨@Noctsol 我认为这并不重要,因为SomeClass.OptionalProperty == null 相当于SomeClass doesn't have OptionalProperty
    • 嗨@Noctsol 如果对这篇文章没有任何疑问,请您将我的回答标记为“已接受”,提前谢谢~
    • 完成。这个答案实际上帮助我找到了答案。制作自定义反序列化/序列化协议。
    猜你喜欢
    • 2016-11-04
    • 2015-03-18
    • 1970-01-01
    • 2020-05-26
    • 1970-01-01
    • 2021-09-30
    • 1970-01-01
    • 2012-04-14
    • 2018-09-28
    相关资源
    最近更新 更多