JamesNK / Newtonsoft.Json

Json.NET is a popular high-performance JSON framework for .NET
https://www.newtonsoft.com/json
MIT License
10.73k stars 3.25k forks source link

[Feature Request] MissingMemberHandling attribute #2975

Open kikniknik opened 1 month ago

kikniknik commented 1 month ago

It would be useful to expose MissingMemberHandling serialization setting as a JsonPropertyAttribute, in order to override it for specific members only.

System.Text.Json does it with JsonUnmappedMemberHandling attribute.

elgonzo commented 1 month ago

Did you perhaps mistakenly confuse JsonPropertyAttribute with JsonObjectAttribute?

A JsonPropertyAttribute is being applied to a C#/.NET field or property. MissingMemberHandling deals with situations where no suitable C#/.NET property exists for some json property.

Therefore, MissingMemberHandling doesn't make any sense in the context of JsonPropertyAttribute, because to apply a JsonPropertyAttribute, the C#/.NET field/property must exist, obviously, so there is no situation where the C#/.NET field/property would be missing.

Also, STJ's [JsonUnmappedMemberHandling] attribute applies to types, not fields/properties. I fail to see how you would relate its functionality to the [JsonProperty] attribute which applies to properties/fields and not types.


All that said, please note that i am not the author/maintainer of Newtonsoft.Json and not associated with the project in any form (i am just a -mostly former- user of Newtonsoft.Json. If you check the commit history as well as the activity of the author of this library here in the Github repo, you will notice that the library basically is legacy and in what i would call "maintenance mode". So, even if your feature suggestions would make sense, it would be extremely unlikely that it would become reality due to the lack of substantial development activities regarding this library. In my opinion, if you are able it's better to switch to STJ. (Note that STJ is also available as a nuget package for .NET versions as old as .NET Framwork 4.6.2.)

bartelink commented 1 month ago

"maintenance mode" with a subtle DNR sign on it's locker :P

CZEMacLeod commented 1 month ago

To follow up on this: [JsonObject(MissingMemberHandling = MissingMemberHandling.Error)] is the equivalent of [JsonUnmappedMemberHandling(JsonUnmappedMemberHandling.Disallow)] for a specific type.

I think the request is for something like:

public class AnObject
{
    public int ID { get; set; }
    public string Name { get; set; }
}

public class AnObjectWithErrors
{
    [JsonMissingMember(MissingMemberHandling.Ignore)]
    public AnObject Object1 { get; set; }

    [JsonMissingMember()]
    public AnObject Object2 { get; set; }
}

Such that if the json for Object2 includes extra members, regardless of the setting of JsonSerializerSettings.MissingMemberHandling, that it fails (and in the case of Object1 that it always succeeds).

I think the idea would be to override both the JsonSerializerSettings and JsonObject for that specific property.

I'm not entirely sure just how useful this would be, or under what circumstances you would want to use it, but it can be done.

using Newtonsoft.Json;

public class MissingMemberConverter : JsonConverter
{
    private readonly MissingMemberHandling missingMemberHandling;

    private readonly JsonConverter? baseConverter;

    public MissingMemberConverter() : this(MissingMemberHandling.Error) { }

    public MissingMemberConverter(MissingMemberHandling missingMemberHandling) => this.missingMemberHandling = missingMemberHandling;

    public MissingMemberConverter(Type baseConverter, params object[]? baseConverterArgs) : 
        this(MissingMemberHandling.Error, baseConverter, baseConverterArgs) { }

    public MissingMemberConverter(MissingMemberHandling missingMemberHandling, Type baseConverter, params object[]? baseConverterArgs) : 
        this(missingMemberHandling) => 
            this.baseConverter = (JsonConverter?)Activator.CreateInstance(baseConverter, baseConverterArgs);

    public override bool CanConvert(Type objectType) => baseConverter is null || baseConverter.CanConvert(objectType);

    public override object? ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer)
    {
        if (reader.TokenType == JsonToken.Null) return null;

        JsonConverter? converter = baseConverter?.CanRead ?? false ? baseConverter : null;
        if (baseConverter is null) { 
            var contract = serializer.ContractResolver.ResolveContract(objectType);
            converter = contract.Converter ?? serializer.GetMatchingConverter(contract.UnderlyingType) as JsonConverter ?? contract.InternalConverter;
        }

        var previousHandling = serializer.MissingMemberHandling;
        serializer.MissingMemberHandling = missingMemberHandling;
        try
        {
            var value = converter is null ?
                serializer.Deserialize(reader, objectType) :
                converter.ReadJson(reader, objectType, existingValue, serializer);
            return value;
        }
        catch (Newtonsoft.Json.JsonSerializationException ex)
        {
            serializer.TraceWriter?.Trace(System.Diagnostics.TraceLevel.Error, "Serialization Exception in MissingMemberConverter", ex);
            throw;
        }
        finally
        {
            serializer.MissingMemberHandling = previousHandling;
        }
    }

    public override bool CanRead => true;

    public override bool CanWrite => baseConverter?.CanWrite ?? false;

    public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer)
    {
        if (baseConverter is null) throw new NotImplementedException();
        baseConverter.WriteJson(writer, value, serializer);
    }
}
static class JsonSerializerExtensions
{

    public static JsonConverter? GetMatchingConverter(this JsonSerializer serializer, Type objectType)
    {
        for (int i = 0; i < serializer.Converters.Count; i++)
        {
            JsonConverter converter = serializer.Converters[i];

            if (converter.CanConvert(objectType))
            {
                return converter;
            }
        }
        return null;
    }
}

Use as follows:

using Newtonsoft.Json;
using Newtonsoft.Json.Converters;
using Newtonsoft.Json.Serialization;

public static class Program
{
    public static void Main()
    {
        var aowe = new AnObjectWithErrors
        {
            Object1 = new AnObject
            {
                ID = 1,
                Name = "a",
            },
            Object2 = new AnObject
            {
                ID = 2,
                Name = "b",
            },
            MissingMemberHandling = MissingMemberHandling.Error
        };
        Console.WriteLine(JsonConvert.SerializeObject(aowe)); // yields {"Object1":{"ID":1,"Name":"a"},"Object2":{"ID":2,"Name":"b"}}

        aowe = TryDeser("{\"Object1\":{\"ID\":1,\"Name\":\"a\"},\"Object2\":{\"ID\":2,\"Name\":\"b\"},\"MissingMemberHandling\":\"error\"}", "Good");
        aowe = TryDeser("{\"Object1\":{\"ID\":1,\"Name\":\"a\",\"foo\":\"bar\"},\"Object2\":{\"ID\":2,\"Name\":\"b\"},\"MissingMemberHandling\":\"ignore\"}", "Object1");
        aowe = TryDeser("{\"Object1\":{\"ID\":1,\"Name\":\"a\",\"foo\":\"bar\"},\"Object2\":{\"ID\":2,\"Name\":\"b\"},\"MissingMemberHandling\":\"ignore\"}", "Object1", MissingMemberHandling.Error);
        aowe = TryDeser("{\"Object1\":{\"ID\":1,\"Name\":\"a\"},\"Object2\":{\"ID\":2,\"Name\":\"b\",\"foo\":\"bar\"},\"MissingMemberHandling\":\"ignore\"}", "Object2");
        aowe = TryDeser("{\"Object1\":{\"ID\":1,\"Name\":\"a\",\"foo\":\"bar\"},\"Object2\":{\"ID\":2,\"Name\":\"b\",\"foo\":\"bar\"},\"MissingMemberHandling\":\"ignore\"}", "Both");

    }

    private static AnObjectWithErrors? TryDeser(string value, string description, MissingMemberHandling missingMemberHandling = MissingMemberHandling.Ignore)
    {
        try
        {
            Console.WriteLine($"Deserializing {description}");
            Console.WriteLine(JsonConvert.SerializeObject(JsonConvert.DeserializeObject(value), Formatting.Indented));
            var ser = new JsonSerializerSettings { MissingMemberHandling = missingMemberHandling };
            var aowe = JsonConvert.DeserializeObject<AnObjectWithErrors>(value, ser);
            Console.WriteLine($"{description} Succeeded");
        }
        catch (Exception ex)
        {
            Console.WriteLine($"{description} Failed {ex.Message}");
        }

        return null;
    }
}

public class AnObject
{
    public int ID { get; set; }
    public string Name { get; set; }
}

public class AnObjectWithErrors
{
    [JsonConverter(typeof(MissingMemberConverter), MissingMemberHandling.Ignore)]   // Note this applies to this property and all nested objects
    public AnObject Object1 { get; set; }

    [JsonConverter(typeof(MissingMemberConverter))]   // Note this applies to this property and all nested objects
    public AnObject Object2 { get; set; }

    // This doesn't really make sense - but shows how a nested converter may be specified
    [JsonConverter(typeof(MissingMemberConverter), typeof(StringEnumConverter), new object[] { typeof(CamelCaseNamingStrategy) })]
    public MissingMemberHandling MissingMemberHandling { get; set; }
}

Note that this will still fail on Object1 if you use

[JsonObject(MissingMemberHandling = MissingMemberHandling.Error)]
public class AnObject
{
    public int ID { get; set; }
    public string Name { get; set; }
}

I think there is probably a way round that - but since this was for proof of concept - I leave that as an exercise for the reader.

Unfortunately JsonConverterAttribute is sealed so we cannot do something like this

[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property, AllowMultiple = false)]
public class JsonMissingMemberAttribute : JsonConverterAttribute
{
    public JsonMissingMemberAttribute(MissingMemberHandling missingMemberHandling = MissingMemberHandling.Error) :
        base(typeof(MissingMemberConverter), missingMemberHandling)
    { }

    public JsonMissingMemberAttribute(Type baseConverter, params object[]? baseParams) : base(typeof(MissingMemberConverter), baseConverter, baseParams)
    { }

    public JsonMissingMemberAttribute(MissingMemberHandling missingMemberHandling, Type baseConverter, params object[]? baseParams) : 
        base(missingMemberHandling, typeof(MissingMemberConverter), baseConverter, baseParams)
    { }
}
kikniknik commented 1 month ago

@CZEMacLeod this example:

public class AnObject
{
    public int ID { get; set; }
    public string Name { get; set; }
}

public class AnObjectWithErrors
{
    [JsonMissingMember(MissingMemberHandling.Ignore)]
    public AnObject Object1 { get; set; }

    [JsonMissingMember()]
    public AnObject Object2 { get; set; }
}

is exactly what I have in mind for usage of MissingMemberHandling attribute.

I find it useful in cases when you generally don't want to check for missing members but after a change in a property name (and type possibly), you want to make sure that old name is not used by mistake.

I think your implementation of JsonConverter does what a MissingMemberHandling attribute should and I will use it. Thank you very much!

As for comparison of this library to System.Text.Json, I can see that this repo is for long period now practically inactive, but still in most cases I find Newtonsoft to be more reasonable and easier to work with. For example serializing double 5.0 to 5.0 and not 5, having CanRead, CanWrite to JsonConverter, easier navigation in a JObject.