1

I want to slightly tweak serialization result in a non-intrusive way.
For example, I want this class:

class A { int va; }

to be modified like this { va: value } -> { va: value * 2 }
So I tried to make a converter, but the only way I found like this:

    [JsonConverter(typeof(NoConverter))]
    class B : A { }
    public class MyConverter : JsonConverter<A> {
        public override void WriteJson(JsonWriter writer, A obj, JsonSerializer serializer) {
            // serializer.Serialize(writer, obj);
            // serializer.Serialize(writer, new A { va = obj.va * 2 });
            serializer.Serialize(writer, new B { va = obj.va * 2 });
        }
    }

Is there a better way?

Problems of this way:

  1. Every class with converter enabled, must be forked and implement a copy method.
  2. Commented lines does not work because serializer blindly re-invoke converter and stack overflows.
  3. I did not find a way to avoid self recursion.
    If there is a way to force invoke the default conversion, then the self recursion is avoided.

BTW:
Newtonsoft samples on converter are obsolete, and tests in repo not helpful.

Thanks to this SO post's NoConverter, I have at least got a working way.
Why Json.net does not use customized IsoDateTimeConverter?

This SO is interesting, but cannot solve my problem - I need to invoke the default conversion routine, which is not a converter.
Custom JsonConverter WriteJson Does Not Alter Serialization of Sub-properties

user2771324
  • 307
  • 3
  • 13
  • 1
    Does [JSON.Net throws StackOverflowException when using `[JsonConvert()]`](https://stackoverflow.com/q/29719509/3744182) answer your question? Also, why not apply a converter to the property `va` instead of to the class `A`? For instance [here](https://stackoverflow.com/a/32148870/3744182) is an example of an `IntConverter` you could apply directly to `va`: `[JsonConverter(typeof(IntConverter))] int va;` That converter adds 1 but you could multiply by 2 using the same pattern. – dbc Aug 10 '21 at 21:16
  • I would try 1st SO answer which simulates object handling. 2nd answer does not fully apply because I just used int as an strip-down example. I don't know how to write a converter for property, should I let CanXXX() return true or is it irrelevant for property converter? How to get the holding object that the property lives? (I need have to read holder's other properties) – user2771324 Aug 10 '21 at 22:03
  • 1) *should I let CanXXX() return true or is it irrelevant for property converter?* `JsonConverter.CanConvert(Type)` is irrelevant and never called when a converter is applied via attributes to a type or a member. `CanRead` and `CanWrite` return true. 2) *How to get the holding object that the property lives?* -- Need to see a [mcve] but I don't think you can. The converter is passed the property value. You can do tricks with a custom contract resolver and `IValueProvider` though, see [Overriding a property value in custom JSON.net contract resolver](https://stackoverflow.com/q/46977905). – dbc Aug 10 '21 at 22:07
  • *I would try 1st SO answer which simulates object handling.* -- then may this be closed as a duplicate, or do you need more specific help? – dbc Aug 10 '21 at 22:08

2 Answers2

2

The following should work for serialisation.

class A 
{ 
    [JsonIgnore]  
    public int Va; 
    
    [JsonProperty(PropertyName = "Va")]
    private int VaOutput { get { return Va *2; } }
}

So you mark your original property with [JsonIgnore]

Then you define a new private property, which returns you the altered value (*2) with no setter. This is given the property name matching that of your original.

An alternative, is to create another class 'B' which looks the same, accepts Class A as an input in the constructor, sets all the values and alters them as required. You then serialize this class instead of the original.

jason.kaisersmith
  • 8,712
  • 3
  • 29
  • 51
  • Thanks, I was reluctant to use property attribute, because the serialization was on another assembly, depending on [class A] assembly. (But I forgot to update question to show this concern, my fault.) The duplication method was mentioned as my current solution. – user2771324 Aug 11 '21 at 19:14
0

I ended up use a solution like this.
Full sample at https://dotnetfiddle.net/OPoqbs.

    public class SerializerContext {
        public void peekWriter(JsonSerializer serializerProxy) {
            if (writer != null) return;
            if (verifySerializerProxy(serializerProxy)) {
                writer = serializerProxy.GetType().GetField("_serializerWriter", BF.Instance | BF.NonPublic).GetValue(serializerProxy);
                objectsStack = (List<object>)writer.GetType().GetField("_serializeStack", BF.Instance | BF.NonPublic).GetValue(writer);
            }
        }

        public void peekReader(JsonSerializer serializerProxy) {
            if (reader != null) return;
            if (verifySerializerProxy(serializerProxy)) {
                reader = serializerProxy.GetType().GetField("_serializerReader", BF.Instance | BF.NonPublic).GetValue(serializerProxy);
            }
        }

        public bool verifySerializerProxy(JsonSerializer serializerProxy) {
            var assembly = typeof(JsonSerializer).Assembly;
            var serializerProxyType = assembly.GetType("Newtonsoft.Json.Serialization.JsonSerializerProxy");
            return serializerProxyType != null && serializerProxyType.IsInstanceOfType(serializerProxy);
        }

        public object reader = null!; // internal Newtonsoft.Json.Serialization.JsonSerializerInternalReader
        public object writer = null!; // internal Newtonsoft.Json.Serialization.JsonSerializerInternalWriter
        public List<object> objectsStack = null!; // JsonSerializerInternalWriter._serializeStack
    }
    
    // In-place mutate/transform converter. Useful if new value maybe in same type, or maybe same object.
    public abstract class InPlaceMutateConverter : JsonConverter {
        public override bool CanRead {
            get {
                // Avoid next recurse to self
                if (avoidNextRecurse) {
                    avoidNextRecurse = false;
                    return false;
                } else {
                    return canRead;
                }
            }
        }

        public override bool CanWrite {
            get {
                // Avoid next recurse to self
                if (avoidNextRecurse) {
                    avoidNextRecurse = false;
                    return false;
                } else {
                    return canWrite;
                }
            }
        }

        public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) {
            serializerContext.peekReader(serializer);
            Type newObjectType = GetOutputTypeOnRead(objectType, existingValue, out avoidNextRecurse);
            object value = serializer.Deserialize(reader, newObjectType)!;
            return MutateOnRead(value);
        }

        public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) {
            if (objectsStack == null) {
                serializerContext.peekWriter(serializer);
                objectsStack = serializerContext.objectsStack;
            }
            object newValue = MutateOnWrite(value, out avoidNextRecurse);
            // To defeat circular check
            if (newValue == value)
                objectsStack.Remove(objectsStack.Count - 1);
            serializer.Serialize(writer, newValue);
            // Restore stack
            if (newValue == value)
                objectsStack.Add(value);
        }

        protected virtual Type GetOutputTypeOnRead(Type objectType, object existingValue, out bool typeWillMatchSelf) {
            typeWillMatchSelf = true;
            return objectType;
        }
        protected virtual object MutateOnRead(object value) { return value; }
        // typeWillMatchSelf should be true, if converter will recurse to self: value type will still hit self.
        protected virtual object MutateOnWrite(object value, out bool typeWillMatchSelf) {
            typeWillMatchSelf = true;
            return value;
        }

        protected bool canRead = true;
        protected bool canWrite = true;
        protected SerializerContext serializerContext = new SerializerContext();
        List<object> objectsStack = null!;
        protected bool avoidNextRecurse = false;
    }

Pros:

  1. Serialization code separates from serialized classes.
  2. Straightforward. Avoid fiddling with over-complicated resolver/provider.
    (over-complicated => solution too complex for a simple problem)

Cons:

  1. Reflection is used.
    I am fine with reflection because it is a shortcut to fork repo and modify.
  2. .CanRead/.CanWrite looks nasty.
    But I didn't come up with better solution. IMO it had better to add a few straight forward callbacks.
user2771324
  • 307
  • 3
  • 13