简体   繁体   中英

JSON deserialization - Map array indices to properties with JSON.NET

I want to deserialize a 2-dimensional array to a collection of .net objects. The reason is, array syntax will be easier for my user to work with in an input file. So I just want to map the indices of the arrays to specific properties of my target type.

EG With:

[
     ["John", "Smith", "23"],
     ["Paula", "Martin", "54]
]

I would get two instances of a Person:

public class Person {
    public string First {get;set;}
    public string Last {get;set;}
    public string Age {get;set;}
}

where index 0 of an inner array maps to First , index 1 maps to Last , and index 2 maps to Age ;

Is there a way to extend Json.NET so that I can do the mapping during deserialization so the implementation details are hidden? I have been playing around with a custom JsonConverter but I haven't found much info on how to use it.

Edit: Specifically, I'm not sure if JsonConverter is the right thing to use, and I'm having trouble figuring out how to implement CanConvert and how to use the parameters passed to the ReadJson method.

You can do this with a JsonConverter . A simple converter for this purpose would be:

public class PersonConverter : JsonConverter
{
    public override bool CanConvert(Type objectType)
    {
        return objectType == typeof(Person);
    }

    public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
    {
        if (reader.TokenType == JsonToken.Null)
            return null;
        var array = JArray.Load(reader);
        var person = (existingValue as Person ?? new Person());
        person.First = (string)array.ElementAtOrDefault(0);
        person.Last = (string)array.ElementAtOrDefault(1);
        person.Age = (string)array.ElementAtOrDefault(2);
        return person;
    }

    public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
    {
        var person = (Person)value;
        serializer.Serialize(writer, new[] { person.First, person.Last, person.Age });
    }
}

If the specific properties have non-primitive types, you can use JToken.ToObject<T>(JsonSerializer) to deserialize them to the required type:

person.First = array.ElementAtOrDefault(0)?.ToObject<string>(serializer);

Then you can apply it to your class:

[JsonConverter(typeof(PersonConverter))]
public class Person
{
    public string First { get; set; }
    public string Last { get; set; }
    public string Age { get; set; }
}

Or use it in settings:

var settings = new JsonSerializerSettings { Converters = new [] { new PersonConverter() } };
var list = JsonConvert.DeserializeObject<List<Person>>(json, settings);

For a general purpose converter, here's an implementation that uses attributes to associate the properties:

[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field, AllowMultiple = false)]
public class JsonArrayIndexAttribute : Attribute
{
    public JsonArrayIndexAttribute(int index)
    {
        Index = index;
    }
    
    public int Index { get; }
}

public class JsonArrayConverter<T> : JsonConverter<T>
{
    public override T? ReadJson(JsonReader reader, Type objectType, T? existingValue, bool hasExistingValue, JsonSerializer serializer)
    {
        switch (reader.TokenType)
        {
            case JsonToken.StartArray:
                var members = GetIndexedMembers(objectType);
                var arr = JArray.ReadFrom(reader);
                var fromArray = hasExistingValue ? existingValue! : Activator.CreateInstance<T>()!;
                foreach (var (member, value) in members.Zip(arr))
                {
                    member.setter?.Invoke(fromArray, Convert.ChangeType(value, member.type));
                }
                return fromArray;
            case JsonToken.StartObject:
                var fromObject = hasExistingValue ? existingValue! : Activator.CreateInstance<T>()!;
                serializer.Populate(reader, fromObject);
                return fromObject;
            case JsonToken.Null:
                return default;
            case JsonToken.Undefined:
                return hasExistingValue ? existingValue : default;
            default:
                throw new JsonSerializationException($"Unexpected TokenType: {reader.TokenType}");
        }
    }

    public override void WriteJson(JsonWriter writer, T? value, JsonSerializer serializer)
    {
        if (value == null)
        {
            writer.WriteNull();
            return;
        }
        var members = GetIndexedMembers(value.GetType());
        writer.WriteStartArray();
        foreach (var member in members)
        {
            writer.WriteValue(member.getter?.Invoke(value));
        }
        writer.WriteEndArray();
    }
    
    private IEnumerable<(Type? type, Func<object, object?>? getter, Action<object, object?>? setter)> GetIndexedMembers(Type type)
    {
        var indexedMembers =
            (from member in type.GetMembers()
            let index = (JsonArrayIndexAttribute?)member.GetCustomAttribute(typeof(JsonArrayIndexAttribute))
            where index != null
            select (member, index.Index))
            .ToLookup(x => x.Index, x => x.member);

        return
            (from i in Enumerable.Range(0, indexedMembers.Max(x => x.Key) + 1)
            from m in indexedMembers[i].TakeLast(1).DefaultIfEmpty()
            select CreateAccessors(m));
            
        (Type, Func<object, object?>?, Action<object, object?>?) CreateAccessors(MemberInfo m) => m switch
        {
            PropertyInfo p => (p.PropertyType, obj => p.GetValue(obj), (obj, value) => p.SetValue(obj, value)),
            FieldInfo f => (f.FieldType, obj => f.GetValue(obj), (obj, value) => f.SetValue(obj, value)),
            _ => default,
        };
    }
}

Then to use it in your case:

[JsonConverter(typeof(JsonArrayConverter<Person>))]
public class Person
{
    [JsonArrayIndex(0)]
    public string First { get; set; } = default!;
    
    [JsonArrayIndex(1)]
    public string Last { get; set; } = default!;
    
    [JsonArrayIndex(2)]
    public int Age { get; set; }
}
    public override bool CanConvert(Type objectType)
    {
        if (objectType == typeof(List<Person>)) return true;

        return false;
    }

    public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
    {
        List<Person> persons = new List<Person>();

        JArray personsArray = (JArray)serializer.Deserialize(reader);

        foreach (var personArray in personsArray.Children<JArray>())
        {
            persons.Add(new Person() { 
                         First = personArray[0].Value<string>(),
                         Last = personArray[1].Value<string>(),
                         Age = personArray[2].Value<string>()
                        });
        }

        return persons;
    }

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM