skip to Main Content

I’m accessing an api that for some reason returns false whenever null would be used normally.

I now need a way to deserialize an arbitrary json string into a completely generic type and interpreting JsonTokenType.False as JsonTokenType.Null for any Nullable / reference type property no matter how deep it’s nested.

(I’m targeting net7.0 and netstandard2.0 and use System.Text.Json)


Example data (Needs to work for ANY schema!)

{
   "property": false // expected type: string
   "property2": false // expected type: int
   "property3": { // data might be nested
      "property": false // expected type SomeType
   }
}
public sealed class JsonDefinition {
   public string? Property { get; set; }
   public int? Property2 { get; set; }
   public NestedJsonType? Property { get; set; }
}
public sealed class NestedJsonType{
   public SomeType? Property { get; set; }
}

3

Answers


  1. Chosen as BEST ANSWER

    I guess I'll just go with sth like input.Replace("false", "null") for now...


  2. System.Text.Json is only good for a demo to serialize "Hello World". I tried to use it , but code is getting unnecessary complicated. So I give you a Newtonsoft.Json converter. If you are a fan of Text.Json try to convert to it and you will see what I am talking about

    JsonDefinition data = JsonConvert.DeserializeObject<JsonDefinition>(json, new FalseJsonConverter<JsonDefinition>());
    
    public class FalseJsonConverter<T> : JsonConverter<T> 
    {
        public override void WriteJson(JsonWriter writer, T? value, Newtonsoft.Json.JsonSerializer serializer)
        {
            throw new NotImplementedException();
        }
    
        public override T? ReadJson(JsonReader reader, Type objectType, T? existingValue, bool hasExistingValue, Newtonsoft.Json.JsonSerializer serializer)
        {
            JObject o = JObject.Load(reader);
    
            var falses = o.DescendantsAndSelf()
                        .Where(x => (x.Type == JTokenType.Boolean))
                        .Select(x => (JProperty)x.Parent)
                        .ToList();
    
            for (var i = 0; i < falses.Count(); i++)
                if ((Boolean)falses[i].Value == false) falses[i].Value = null;
    
            return o.ToObject<T>();
        }
    
        public override bool CanRead
        {
            get { return true; }
        }
    }
    
    Login or Signup to reply.
  3. If you are using .NET 7 (or the System.Text.Json nuget version 7) or later, you can add a DefaultJsonTypeInfoResolver modifier that applies a "false as default" converter to every property of every type encountered during deserialization:

    First, create the following Action<JsonTypeInfo> modifier:

    public static partial class JsonExtensions
    {
        public static Action<JsonTypeInfo> AddFalseToDefaultPropertyConverter { get; } = 
            static typeInfo => 
            {
                if (typeInfo.Kind != JsonTypeInfoKind.Object)
                    return;
                foreach (var property in typeInfo.Properties)
                {
                    // TODO: Modify these checks as required.
                    // For instance if PropertyType is declared as typeof(object) or typeof(JsonNode) or typeof(JsonElement?) you may want to let `false` be deserialized as-is.
                    if ((!property.PropertyType.IsValueType || Nullable.GetUnderlyingType(property.PropertyType) != null) && property.CustomConverter == null)
                        property.CustomConverter = (JsonConverter)Activator.CreateInstance(typeof(FalseAsDefaultConverter<>).MakeGenericType(property.PropertyType))!;
                }
            };
        
        class FalseAsDefaultConverter<T> : JsonConverter<T>
        {
            public override T? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) =>
                reader.TokenType switch
                {
                    JsonTokenType.False => default,
                    _ =>JsonSerializer.Deserialize<T>(ref reader, options),
                };
            public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) => 
                JsonSerializer.Serialize(writer, value, options);
        }
    }
    

    And now you will be able to deserialize your JSON as follows:

    var options = new JsonSerializerOptions
    {
        TypeInfoResolver = new DefaultJsonTypeInfoResolver
        {
            Modifiers = { JsonExtensions.AddFalseToDefaultPropertyConverter },
        },
        PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
        ReadCommentHandling = JsonCommentHandling.Skip,
        // Add other options as required:
        WriteIndented = true,
    };
    
    var model = JsonSerializer.Deserialize<JsonDefinition>(input, options);
    

    Notes:

    • Because the converter is applied directly to each serialized property, there is no problem with recursion.

    • This solution does not handle false values for the root object, or for collection items.

    • You may need to tweak the // TODO: Modify these checks as required. logic that selects the property types to which to apply the converter.

    Demo fiddle #1 here.

    If you are using .NET 6 then contract customization is not available. Instead, you could preload into a JsonNode hierarchy and recursively remove all false property values:

    public static partial class JsonExtensions
    {
        public static JsonNode? RemoveFalseProperties(this JsonNode? root)
        {
            foreach (var item in root.DescendantItemsAndSelf(false).Where(i => i.name != null && i.node is JsonValue v && v.TryGetValue<bool>(out var b) && b == false).ToList())
                ((JsonObject)item.parent!).Remove(item.name!);
            return root;
        }
        
        //Taken from https://stackoverflow.com/questions/73887517/how-to-recursively-descend-a-system-text-json-jsonnode-hierarchy-equivalent-to
        /// Recursively enumerates all JsonNodes (including their index or name and parent) in the given JsonNode object in document order.
        public static IEnumerable<(JsonNode? node, int? index, string? name, JsonNode? parent)> DescendantItemsAndSelf(this JsonNode? root, bool includeSelf = true) => 
            RecursiveEnumerableExtensions.Traverse(
                (node: root, index: (int?)null, name: (string?)null, parent: (JsonNode?)null),
                (i) => i.node switch
                {
                    JsonObject o => o.AsDictionary().Select(p => (p.Value, (int?)null, p.Key.AsNullableReference(), i.node.AsNullableReference())),
                    JsonArray a => a.Select((item, index) => (item, index.AsNullableValue(), (string?)null, i.node.AsNullableReference())),
                    _ => i.ToEmptyEnumerable(),
                }, includeSelf);
    
        static IEnumerable<T> ToEmptyEnumerable<T>(this T item) => Enumerable.Empty<T>();
        static T? AsNullableReference<T>(this T item) where T : class => item;
        static Nullable<T> AsNullableValue<T>(this T item) where T : struct => item;
        static IDictionary<string, JsonNode?> AsDictionary(this JsonObject o) => o;
    }
    
    public static partial class RecursiveEnumerableExtensions
    {
        // Rewritten from the answer by Eric Lippert https://stackoverflow.com/users/88656/eric-lippert
        // to "Efficient graph traversal with LINQ - eliminating recursion" http://stackoverflow.com/questions/10253161/efficient-graph-traversal-with-linq-eliminating-recursion
        // to ensure items are returned in the order they are encountered.
        public static IEnumerable<T> Traverse<T>(
            T root,
            Func<T, IEnumerable<T>> children, bool includeSelf = true)
        {
            if (includeSelf)
                yield return root;
            var stack = new Stack<IEnumerator<T>>();
            try
            {
                stack.Push(children(root).GetEnumerator());
                while (stack.Count != 0)
                {
                    var enumerator = stack.Peek();
                    if (!enumerator.MoveNext())
                    {
                        stack.Pop();
                        enumerator.Dispose();
                    }
                    else
                    {
                        yield return enumerator.Current;
                        stack.Push(children(enumerator.Current).GetEnumerator());
                    }
                }
            }
            finally
            {
                foreach (var enumerator in stack)
                    enumerator.Dispose();
            }
        }
    }
    

    And then, to deserialize, do:

    var options = new JsonSerializerOptions
    {
        PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
        ReadCommentHandling = JsonCommentHandling.Skip,
        // Add other options as required:
        WriteIndented = true,
    };
    
    var model = JsonNode.Parse(input, documentOptions : new() { CommentHandling = JsonCommentHandling.Skip })
        .RemoveFalseProperties()
        .Deserialize<JsonDefinition>(options);
    

    Notes:

    • This solution is more robust than a simple string replacement such as input.Replace("false", "null") since it is quite possible that the text false may appear within some JSON string.

    • System.Text.Json.Nodes lacks an equivalent to JContainer.Descendants() so we have to write one ourselves.

    Demo #2 here.

    I removed false properties because it seemed more robust than replacing them with null. If you would prefer to replace with null you will need to add in a JsonConverter<bool> that maps null to false for bool values.

    public class BoolConverter : JsonConverter<bool>
    {
        public override void Write(Utf8JsonWriter writer, bool value, JsonSerializerOptions options) =>
            writer.WriteBooleanValue(value);
        
        public override bool Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) =>
            reader.TokenType switch
            {
                JsonTokenType.True => true,
                JsonTokenType.False => false,
                JsonTokenType.Null => false,
                _ => throw new JsonException(),
            };
    }
    
    public static partial class JsonExtensions
    {
        public static JsonNode? ReplaceFalseWithNullProperties(this JsonNode? root)
        {
            foreach (var item in root.DescendantItemsAndSelf(false).Where(i => i.name != null && i.node is JsonValue v && v.TryGetValue<bool>(out var b) && b == false).ToList())
                ((JsonObject)item.parent!)[item.name!] = null;
            return root;
        }
    }
    

    And then deserialize like so:

    var options = new JsonSerializerOptions
    {
        PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
        ReadCommentHandling = JsonCommentHandling.Skip,
        Converters = { new BoolConverter() },
        // Add other options as required:
        WriteIndented = true,
    };
    
    var model = JsonNode.Parse(input, documentOptions : new() { CommentHandling = JsonCommentHandling.Skip })
        .ReplaceFalseWithNullProperties()
        .Deserialize<JsonDefinition>(options);
    

    Demo #3 here.

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