JamesNK / Newtonsoft.Json

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

Access the parent object in a converter #1543

Open tankbob opened 6 years ago

tankbob commented 6 years ago

Stack overflow example

NewtonSoft JsonConverter - Access other properties

Problem

There is no way to access the parent object from a JsonConverter. For example if there was a need to alter the output of the converter that is dependent on containing object that is being serialized. In my specific use case, I need to format the JSON output for a property as a currency string, dependent on the Culture that is stored within the containing object.

eg.

public class Contract {
   [JsonIgnore]
   public CultureInfo Culture {get; set;}

   [JsonConverter(typeof(CurrencyConverter))]
   public decimal Cost {get;set;}

   // Lots of additional properties with serialization controlled by attributes.
}

With an array of contracts I'd get the following JSON [{ Cost : "£5000.00"}, { Cost : "$8000.00"}, { Cost : "€599.00"}]

Suggested solution

Expose the JsonSerializeWriter's _serializerStack as a ReadOnlyCollection as a public property on the serializer object passed to WriteJson.

JonHanna commented 6 years ago

If the output of the converter depends on the containing type, why not have the containing type be what is converted?

tankbob commented 6 years ago

@JonHanna Because that means you need to handle the conversion of all the properties in the containing object rather that relying on the default behaviour using properties.

Also if you had a Contract with many Vendors each with many Assets and the Asset had a cost field that needed the output formatted depending on the Contract Culture you would have to write a converter for the Contract that then serialized all the child properties. That creates a lot of extra code for the converter where a simple converter could walk the serialization tree to find that culture and format the output.

Your DTO objects would be relatively clean with attributes defining their behaviour and your converter also would be relatively simple without the need to map all the extra properties.

I've got it working by using reflection to call GetInternalSerializer() on the passed JsonSerializer and then retrieve the private _serializerStack field. All it would need is for JsonSerializerInternalWriter to expose the "_serializerStack" field as a property that the JsonSerializer could also expose. My suggestion is to expose it as a read only collection to protect the internals.

hybridherbst commented 3 years ago

I'd like to bump this, as it seems reflecting into the serializer is still the only way to get to the serialization stack. Or is there another way by now?

yfital commented 3 years ago

Another bump, I'll describe my use case, maybe there is another method of achieving this (I'm using GetInternalSerializer via reflection as well for now).

Example:

[MySpecialmarker]
{
public class A
public B[] collection
}

public class B
{
public int[] innerCollection;
}

public class C
{
public A MyA;
}

I have class A. Class A has a special definition which indicates how to treat it's collections. In my case, I want to serialize A with only the first item from its collections. A is sometimes sent as part of other classes (e.g. C) where I want this logic to work. B on the other hand doesn't have the attribute, so it's property should be printed completly.

Limitations. I cannot put any attributes on any property (they are code generated). I can put any attribute on the class itself (as it is partial)

As such, I basically need to find the closest "stack" object above me which is not primitive and check if it has the attribute.

I'd be happy to hear other solutions (or expose it instead of reflection ;))

petriashev commented 3 years ago

@yfital You can create custom JsonConverter for your type and handle attributes. If no attributes found serialize as is: serializer.Serialize(writer, value);

yfital commented 3 years ago

Hey @petriashev, In order to do that, I would have had to implement larger logic (in my eyes) as my specific need is for collections only, meaning I would have had to catch all types in a main convertor just for the sake of "keeping" the parent and than iterate per item (while performing logic between the properties of json serialization) just for the sake of finding a possible collection which i want to handle

It would have meant a more intimate knowledge of how serialization is made instead of just handling the type i need (hope i was clear)

petriashev commented 3 years ago

@yfital, yes I understand your case. All type handler is not a variant. I thought that you can write converter for known type. I faced with somilar problem but I have common type that I can catch

kemsky commented 3 years ago

This is possible for collection properties using trick with the extended collection.

  1. Extend List<T> like this:
    public class CustomList<TElement, TParent> : List<TElement>
    {
        public TParent Parent { get; }

        public CustomList(TParent parent)
        {
                 Parent = parent;
        }
    }
  1. Modify model as follows:

    public class Contract {
    public Contract (){
         Cost = new CustomList<double, Contract>(this);
    }
    
    public CultureInfo Culture {get; set;}
    
    [JsonConverter(typeof(CurrencyConverter))]
    public List<double> Cost {get;set;}
    
    // Lots of additional properties with serialization controlled by attributes.
    }
  2. Write custom CurrencyConverter and access CustomList:
        public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
        {
             var list = (CustomList<double, Contract>) existingValue;
             // list .Parent ....
        }