3

I have a class which needs to serialize objects of any type to JSON. If the object has one or more properties which are of type list I want to serialize the whole object but only serialize the first and last item in the list.

For example, I have the below code

using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.Collections;
using Newtonsoft.Json.Serialization;
using System.Linq;
using Newtonsoft.Json.Linq;

public class Program
{
    public class Product{
        public Product(string name, int price){
            this.Name = name;
            this.Price = price;
        }
        public string Name {get;set;}
        public int Price {get;set;}
    }

    public class ProductResult{
        public ProductResult(List<Product> products, int code){
            this.Products = products;
            this.Code = code;
        }
        public int Code {get;set;}
        public List<Product> Products {get;set;}    
    }

    public static string DoTheThing(object dynamicObject){
        return JsonConvert.SerializeObject(dynamicObject);
    }

    public static void Main()
    {
        var list = new List<Product>(){new Product("product1",100),new Product("product2",100),new Product("product3",100),new Product("product4",100)};
        var result = new ProductResult(list,0);

        string jsonObj = DoTheThing(result);
        Console.WriteLine(jsonObj);
        // Output {"Code":0,"Products":[{"Name":"product1","Price":100},{"Name":"product2","Price":100},{"Name":"product3","Price":100},{"Name":"product4","Price":100}]}
    }
}

I would like it to output the following, and I need it to be able to handle various object types.

{"Code":0,"Products":[{"Name":"product","Price":100},{"Name":"product","Price":100}]}

I took a look at using Custom JsonConverter and Custom ContractResolver but am not sure of how to implement these.

DavidG
  • 113,891
  • 12
  • 217
  • 223
B.L.Coskey
  • 285
  • 2
  • 9

2 Answers2

1

You can do this with a custom JsonConverter, for example:

public class FirstAndLastListConverter : JsonConverter
{
    // We only care about List<> properties
    public override bool CanConvert(Type objectType) => 
        objectType.IsGenericType && objectType.GetGenericTypeDefinition() == typeof(List<>);

    // We are not deserialising, complete this if you really need to, but I don't see why you would
    public override object ReadJson(JsonReader reader, Type objectType, 
        object existingValue, JsonSerializer serializer)
    {
        throw new NotImplementedException();
    }

    public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
    {
        // Convert the value to an IList
        var elements = value as IList;

        // Start a new array
        writer.WriteStartArray();

        // Serialise the first item
        serializer.Serialize(writer, elements[0]);

        // Serialise the last item
        serializer.Serialize(writer, elements[elements.Count - 1]);

        // End the array
        writer.WriteEndArray();
    }
}

And use it like this:

// A settings object to use our new converter
var settings = new JsonSerializerSettings
{
    Converters = new [] { new FirstAndLastListConverter() }
};

// Use the settings when serialising
var json = JsonConvert.SerializeObject(result, settings);
DavidG
  • 113,891
  • 12
  • 217
  • 223
1

You could use a Custom ContractResolver along with a ValueConverter. For example,

public class ListContractResolver : DefaultContractResolver
{
    public new static readonly ListContractResolver  Instance = new ListContractResolver ();

    protected override JsonProperty CreateProperty(MemberInfo member, MemberSerialization memberSerialization)
    {
        JsonProperty property = base.CreateProperty(member, memberSerialization);
        if (property.PropertyType.IsGenericType &&   property.PropertyType.GetInterfaces().Contains(typeof(IList)))
        {
           property.ValueProvider = new FilterListValueProvider(member as PropertyInfo);
        }
        return property;
    }
}

public class FilterListValueProvider : IValueProvider
{
    PropertyInfo _propertyInfo;
    public FilterListValueProvider(PropertyInfo propertyInfo)
    {
        _propertyInfo = propertyInfo;
    }

    public object GetValue(object target)
    {
        return GetList(target);
    }

    public IEnumerable GetList(object target)
    {
        var list = _propertyInfo.GetValue(target) as IList;
        yield return list[0];
        yield return list[list.Count - 1];
    }

    public void SetValue(object target, object value)
    {
        _propertyInfo.SetValue(target, value);
    }
}

Updating your DoTheThing method to use the ListContractResolver we defined as

public static string DoTheThing(object dynamicObject)
{
        return JsonConvert.SerializeObject(dynamicObject,
                                   Newtonsoft.Json.Formatting.Indented, 
                                   new JsonSerializerSettings 
                                   { ContractResolver = new ListContractResolver() });
}

Sample Output

{
  "Code": 0,
  "Products": [
    {
      "Name": "product1",
      "Price": 100
    },
    {
      "Name": "product4",
      "Price": 100
    }
  ]
}
DavidG
  • 113,891
  • 12
  • 217
  • 223
Anu Viswan
  • 17,797
  • 2
  • 22
  • 51
  • 1
    Must admit I'm not a fan of the `Activator.CreateInstance` code here.I'd probably have tried to make an `IEnumerable` with `yield` or something like that. – DavidG Mar 09 '20 at 14:54
  • 1
    For clarity, this is what I mean, see how the code is much simpler, smaller and doesn't create a new List object? https://dotnetfiddle.net/kdw0pB – DavidG Mar 09 '20 at 15:01
  • @DavidG Thank You David. Have updated the code with your suggestion. That definetly made it a lot more simpler. – Anu Viswan Mar 09 '20 at 15:09
  • 1
    No problem, I edited out a line I left in! Much better, though I still prefer my version ;) – DavidG Mar 09 '20 at 15:15
  • Thanks for the assistance. Really helpful to see the ContractResolver implementation as well. – B.L.Coskey Mar 09 '20 at 18:10