skip to Main Content

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

2

Answers


  1. this works for me

    public class Tmp2
    {
        public  List<string> IsValChanged { get; private set; } = new();
    
        private int _a;
        public int A
        {
            get { return _a; }
            set
            {
                 _a = value; IsValChanged.Add(nameof(A));
            }
        }
        
        private int? _b;
        public int? B
        {
            get { return _b; }
            set
            {
               _b = value; IsValChanged.Add(nameof(B));
            }
        }
        
        public bool IsPropertyChanged(object prop, [CallerArgumentExpression("prop")] string propName = null)
        {
            return IsValChanged.Any(x => x == propName.Substring(propName.IndexOf(".") + 1));
        }
    }
    

    tests

        var json = "{"A":0}";
        var tmp1 = JsonConvert.DeserializeObject<Tmp2>(json);
        var valueChangedList= string.Join(", ",  tmp1.IsValChanged);
        
        Console.WriteLine(valueChangedList);
        Console.WriteLine(tmp1.IsPropertyChanged(tmp1.A));
        Console.WriteLine(tmp1.IsPropertyChanged(tmp1.B));
        
         json = "{"B":null}";
        var tmp2 = JsonConvert.DeserializeObject<Tmp2>(json);
        valueChangedList = string.Join(", ", tmp2.IsValChanged);
        
        Console.WriteLine(valueChangedList);
        Console.WriteLine(tmp2.IsPropertyChanged(tmp2.A));
        Console.WriteLine(tmp2.IsPropertyChanged(tmp2.B));
    

    test results

    A
    True
    False
    B
    False
    True
    
    Login or Signup to reply.
  2. You could adopt the Optional<T> pattern from the question Custom JSON serializer for optional property with System.Text.Json 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.

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search