区分 JSON 中的跳过值和空值

huangapple go评论60阅读模式
英文:

Differentiate between skipped value and null value in json

问题

I'd like to know if a certain property in JSON was skipped or was provided null. For this, I'm using a setter flag like this. This works fine but it's much uglier, and I will have to create flags for every property I want to check. Is there a neater solution, like creating a custom class that has functions like isSet and Value?

public class Tmp2
{
private int a;
public bool isASet;

private int? b;
public bool isBSet;

public int A { get { return a; } 
    set { a = value; isASet = true; } }
public int? B { get { return b; } 
    set { b = value; isBSet = true; } }

}

Looking for a better solution

英文:

I'd like to know if a certain property in json was skipped or was provided null. For this I'm using a setter flag like this. This works fine but its much uglier and I will have to create flags for every property I want to check. Is there a neater solution, create a custom class that has functions like isSet, Value?

    public class Tmp2
    {
        private int a;
        public bool isASet;

        private int? b;
        public bool isBSet;

        public int A { get { return a; } 
            set { a = value; isASet = true; } }
        public int? B { get { return b; } 
            set { b = value; isBSet = true; } }
    } 

Looking for a better solution

答案1

得分: 2

你可以采用问题 https://stackoverflow.com/q/63418549/3744182 中的 Optional<T> 模式,由 Maxime Rossini 提出,将你的值包装在一个 Optional<T> 结构中,以跟踪值是否被初始化。由于你正在使用 Json.NET,你需要从 System.Text.Json 迁移其逻辑。

首先,定义以下接口、结构和转换器:

public interface IHasValue
{
    bool HasValue { get; }
    object? GetValue();
}

[JsonConverter(typeof(OptionalConverter))]
public readonly struct Optional<T> : IHasValue
{
    // 从 https://stackoverflow.com/q/63418549/3744182 获取
    // 作者 https://stackoverflow.com/users/547733/maxime-rossini
    public Optional(T value) => (this.HasValue, this.Value) = (true, value);
    public bool HasValue { get; }
    public T Value { get; }
    object? IHasValue.GetValue() => Value;
    public override string ToString() => this.HasValue ? (this.Value?.ToString() ?? "null") : "unspecified";
    public static implicit operator Optional<T>(T value) => new Optional<T>(value);
    public static implicit operator T(Optional<T> value) => value.Value;
}

class OptionalConverter : JsonConverter
{
    static Type? GetValueType(Type objectType) =>
        objectType.IsGenericType && objectType.GetGenericTypeDefinition() == typeof(Optional<>) ? objectType.GetGenericArguments()[0] : null;
    
    public override bool CanConvert(Type objectType) => GetValueType(objectType) != null;

    public override object? ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer)
    {
        var valueType = GetValueType(objectType) ?? throw new ArgumentException(objectType.ToString());
        var value = serializer.Deserialize(reader, valueType);
        return Activator.CreateInstance(objectType, value);
    }

    public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer)
        => serializer.Serialize(writer, ((IHasValue?)value)?.GetValue());
}

现在修改你的类(这里是 Tmp2),将要跟踪其存在的每个属性的值替换为 Optional<T> 包装器,如下所示:

public class Tmp2
{
    [JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)] 
    public Optional<int> A { get; set; }
    [JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)]
    public Optional<int?> B { get; set; }
}

现在,你可以通过检查 value.HasValue 来确定任何特定值是否已设置,例如:

Assert.That(!JsonConvert.DeserializeObject<Tmp2>("{}")!.A.HasValue);
Assert.That(JsonConvert.SerializeObject(new Tmp2 { B = 32 }) == "{\"B\":32}");

为了在序列化时抑制未设置属性的输出,你有两个选项:

  1. 使用 [[JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)] ](https://www.newtonsoft.com/json/help/html/T_Newtonsoft_Json_DefaultValueHandling.htm) 标记每个属性:

    当序列化对象时,忽略成员值与成员的默认值相同的成员,以便不将其写入 JSON。

  2. 引入自定义合同解析器,自动抑制未设置属性值的序列化。你可以按照这个自定义合同解析器 的示例进行操作。

上面的 Tmp2 版本使用了第一种方法,但如果你更喜欢第二种方法,请定义以下合同解析器:

public class OptionalResolver : DefaultContractResolver
{
    protected override JsonProperty CreateProperty(MemberInfo member, MemberSerialization memberSerialization)
    {
        var property = base.CreateProperty(member, memberSerialization);
        if (property.ValueProvider != null && property.Readable && (typeof(IHasValue).IsAssignableFrom(property.PropertyType) || typeof(IHasValue).IsAssignableTo(property.PropertyType)))
        {
            var old = property.ShouldSerialize;
            Predicate<object> shouldSerialize = (o) => property.ValueProvider.GetValue(o) is IHasValue v ? v.HasValue : true;
            property.ShouldSerialize = (old == null ? shouldSerialize : (o) => old(o) && shouldSerialize(o));
        }
        return property;
    }
}

然后,简化你的模型如下:

public record Tmp2(Optional<int> A, Optional<int?> B);

现在,你可以使用以下设置来往返 Tmp2 并跟踪属性的存在:

// 在性能最佳的地方将其静态缓存
IContractResolver resolver = new OptionalResolver
{
    // 根据需要配置,例如:
    NamingStrategy = new CamelCaseNamingStrategy(),
};
var settings = new JsonSerializerSettings { ContractResolver = resolver };

var tmp = JsonConvert.DeserializeObject<Tmp2>(json, settings);
var newJson = JsonConvert.SerializeObject(tmp, settings);
Assert.That(JToken.DeepEquals(JToken.Parse(json), JToken.Parse(newJson)));

演示示例在这里

英文:

You could adopt the Optional<T> pattern from the question https://stackoverflow.com/q/63418549/3744182 by Maxime Rossini to wrap your values in an Optional<T> struct that tracks whether or not the value was ever initialized. Since you are using Json.NET you will need to port its logic from System.Text.Json.

First, define the following interface, struct and converter:

public interface IHasValue
{
	bool HasValue { get; }
	object? GetValue();
}

[JsonConverter(typeof(OptionalConverter))]
public readonly struct Optional<T> : IHasValue
{
	//Taken from https://stackoverflow.com/q/63418549/3744182
	//By https://stackoverflow.com/users/547733/maxime-rossini
	public Optional(T value) => (this.HasValue, this.Value) = (true, value);
	public bool HasValue { get; }
	public T Value { get; }
	object? IHasValue.GetValue() => Value;
	public override string ToString() => this.HasValue ? (this.Value?.ToString() ?? "null") : "unspecified";
	public static implicit operator Optional<T>(T value) => new Optional<T>(value);
	public static implicit operator T(Optional<T> value) => value.Value;
}

class OptionalConverter : JsonConverter
{
	static Type? GetValueType(Type objectType) =>
		objectType.IsGenericType && objectType.GetGenericTypeDefinition() == typeof(Optional<>) ? objectType.GetGenericArguments()[0] : null;
		
    public override bool CanConvert(Type objectType) => GetValueType(objectType) != null;

    public override object? ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer)
	{
		var valueType = GetValueType(objectType) ?? throw new ArgumentException(objectType.ToString());
		var value = serializer.Deserialize(reader, valueType);
		return Activator.CreateInstance(objectType, value);
	}

    public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer)
		=> serializer.Serialize(writer, ((IHasValue?)value)?.GetValue());
}

Now modify your classes (here Tmp2) and replace the value of every property whose presence you want to track with an Optional<T> wrapper like so:

public class Tmp2
{
	[JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)] 
    public Optional<int> A { get; set; }
	[JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)]
    public Optional<int?> B { get; set; }
} 

And now you will be able to tell whether any particular value was ever set by checking value.HasValue, e.g.:

Assert.That(!JsonConvert.DeserializeObject<Tmp2>("{}")!.A.HasValue);
Assert.That(JsonConvert.SerializeObject(new Tmp2 { B = 32 }) == "{\"B\":32}");

In order to suppress output of unset properties when serialized, you have two options:

  1. Mark each property with `[JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)] :

    > Ignore members where the member value is the same as the member's default value when serializing objects so that it is not written to JSON.

  2. Introduce a custom contract resolver that automatically suppresses serialization of unset property values.

The version of Tmp2 above uses approach #1 but if you prefer #2, define the following contract resolver:

public class OptionalResolver : DefaultContractResolver
{
    protected override JsonProperty CreateProperty(MemberInfo member, MemberSerialization memberSerialization)
	{
		var property = base.CreateProperty(member, memberSerialization);
		if (property.ValueProvider != null && property.Readable && (typeof(IHasValue).IsAssignableFrom(property.PropertyType) || typeof(IHasValue).IsAssignableTo(property.PropertyType)))
		{
			var old = property.ShouldSerialize;
            Predicate<object> shouldSerialize = (o) => property.ValueProvider.GetValue(o) is IHasValue v ? v.HasValue : true;
			property.ShouldSerialize = (old == null ? shouldSerialize : (o) => old(o) && shouldSerialize(o));
		}
		return property;
	}
}

And simplify your model as follows:

public record Tmp2(Optional<int> A, Optional<int?> B);

And you will now be able to round-trip Tmp2 and track the presence of properties using the following settings:

// Cache this statically somewhere for best performance
IContractResolver resolver = new OptionalResolver
{
	// Configure as required, e.g.:
	NamingStrategy = new CamelCaseNamingStrategy(),
};
var settings = new JsonSerializerSettings { ContractResolver = resolver };

var tmp = JsonConvert.DeserializeObject<Tmp2>(json, settings);
var newJson = JsonConvert.SerializeObject(tmp, settings);
Assert.That(JToken.DeepEquals(JToken.Parse(json), JToken.Parse(newJson)));

Demo fiddle here.

答案2

得分: -1

以下是您提供的代码的中文翻译:

如果您不想向您的类添加任何额外的代码,您可以创建一个通用的包装类。这个类可以用于您的任何数据模型。
public class Tmp2
{
    public int A { get; set; }

    public int? B { get; set; }
}

public class Wrapper<T>
{
    public List<string> IsValChanged { get; set; }
    public T Tmp { get; set; }

    private void Init(JObject tmp)
    {
        IsValChanged = tmp.Properties().DescendantsAndSelf().OfType<JProperty>()
                          .Select(jp => jp.Name).ToList();
        Tmp = tmp.ToObject<T>();
    }

    public Wrapper(JObject tmp)
    {
        Init(tmp);
    }
    public Wrapper(string json)
    {
        Init(JObject.Parse(json));
    }

    public bool IsPropertyChanged(object prop, [CallerArgumentExpression("prop")] string propName = null)
    {
        return IsValChanged.Any(x => x == propName.Substring(propName.IndexOf(".") + 1));
    }
}
测试
var json1 = "{\"A\":0}";
var json2 = "{\"B\":null}";
var json3 = "{\"A\":0,\"B\":null}";

var wrapper = new Wrapper<Tmp2>(json2);

Tmp2 tmp = wrapper.Tmp;
var valueChangedList = string.Join(", ", wrapper.IsValChanged);

Console.WriteLine($"Changed properties: {valueChangedList}");
Console.WriteLine($"{nameof(tmp.A)} changed: {wrapper.IsPropertyChanged(tmp.A)}");
Console.WriteLine($"{nameof(tmp.B)} changed: {wrapper.IsPropertyChanged(tmp.B)}");
测试结果
Changed properties: B
A changed: False
B changed: True
如何在控制器操作中使用包装器
public ActionResult Post([FromBody] JObject model)
{
    var wrapper = new Wrapper<Tmp2>(model);
    Tmp2 tmp = wrapper.Tmp;
    var valueChangedList = string.Join(", ", wrapper.IsValChanged);
    // 另一些代码
}

请注意,我已经将代码部分从您的文本中提取并翻译了它们。如果您需要更多帮助,请随时提问。

英文:

if you don't want to add any extra code to your class, you can create a generic wrapper class. This class can be used for any of your data models.

public class Tmp2
{
	public int A { get; set; }

	public int? B { get; set; }
}

public class Wraper&lt;T&gt;
{
	public List&lt;string&gt; IsValChanged { get; set; }
	public T Tmp { get; set; }
	
	private void Init(JObject tmp)
	{
		IsValChanged = tmp.Properties().DescendantsAndSelf().OfType&lt;JProperty&gt;()
                          .Select(jp =&gt; jp.Name).ToList();
		Tmp = tmp.ToObject&lt;T&gt;();
	}
	
    public Wraper(JObject tmp)
	{
		Init(tmp);
	}
	public Wraper(string json)
	{
		Init(JObject.Parse(json));
	}

	public bool IsPropertyChanged(object prop, [CallerArgumentExpression(&quot;prop&quot;)] string propName = null)
	{
		return IsValChanged.Any(x =&gt; x == propName.Substring(propName.IndexOf(&quot;.&quot;) + 1));
	}
}

test

	var json1 = &quot;{\&quot;A\&quot;:0}&quot;;
	var json2 = &quot;{\&quot;B\&quot;:null}&quot;;
	var json3 = &quot;{\&quot;A\&quot;:0,\&quot;B\&quot;:null}&quot;;

	var wrapper = new Wraper&lt;Tmp2&gt;(json2);

	Tmp2 tmp = wrapper.Tmp;
	var valueChangedList = string.Join(&quot;, &quot;, wrapper.IsValChanged);

	Console.WriteLine($&quot;Changed properties: {valueChangedList}&quot;);
	Console.WriteLine($&quot;{nameof(tmp.A)} changed: {wrapper.IsPropertyChanged(tmp.A)}&quot;);
	Console.WriteLine($&quot;{nameof(tmp.B)} changed: {wrapper.IsPropertyChanged(tmp.B)}&quot;);

test result

Changed properties: B
A changed: False
B changed: True

and how to use a wrapper in a controller action

public ActionResult Post([FromBody] JObject model)
{
	var wrapper = new Wraper&lt;Tmp2&gt;(model);
	Tmp2 tmp = wrapper.Tmp;
	var valueChangedList = string.Join(&quot;, &quot;, wrapper.IsValChanged);
	// another code
}

huangapple
  • 本文由 发表于 2023年5月11日 01:53:26
  • 转载请务必保留本文链接:https://go.coder-hub.com/76221332.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定