21

I have approximately the following picture:

public class Foo
{
   public Foo(Bar bar, String x, String y)
   {
       this.Bar = bar;
       this.X = x;
       this.Y = y;
   }

   [JsonIgnore]
   public Bar Bar { get; private set; }

   public String X { get; private set; }
   public String Y { get; private set; }
}

public class Bar
{
    public Bar(String z)
    {
        this.Z = z;
    }

    public String Z { get; private set; }
}

I want somehow to pass an object of type Bar to a constructor of type Foo during deserialization, i.e:

var bar = new Bar("Hello world");
var x = JsonConvert.DeserializeObject<Foo>(fooJsonString, bar);
John Saunders
  • 160,644
  • 26
  • 247
  • 397
Lu4
  • 14,873
  • 15
  • 79
  • 132

3 Answers3

17

Here are my thoughts regarding problem solution:

The problem:

Json.Net's custom deserialization api is not transparent, i.e. affects my class hierarchy.

Actually it's not a problem in case when you have 10-20 classes in your project, though if you have huge project with thousands of classes, you are not particularly happy about the fact that you need comply your OOP design with Json.Net requirements.

Json.Net is good with POCO objects which are populated (initialized) after they are created. But it's not truth in all cases, sometimes you get your objects initialized inside constructor. And to make that initialization happen you need to pass 'correct' arguments. These 'correct' arguments can either be inside serialized text or they can be already created and initialized some time before. Unfortunately Json.Net during deserialization passes default values to arguments that he doesn't understand, and in my case it always causes ArgumentNullException.

The solution:

Here is approach that allows real custom object creation during deserialization using any set of arguments either serialized or non-serialized, the main problem is that the approach sub-optimal, it requires 2 phases of deserialization per object that requires custom deserialization, but it works and allows deserializing objects the way you need it, so here goes:

First we reassemble the CustomCreationConverter class the following way:

public class FactoryConverter<T> : Newtonsoft.Json.JsonConverter
{
    /// <summary>
    /// Writes the JSON representation of the object.
    /// </summary>
    /// <param name="writer">The <see cref="JsonWriter"/> to write to.</param>
    /// <param name="value">The value.</param>
    /// <param name="serializer">The calling serializer.</param>
    public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
    {
        throw new NotSupportedException("CustomCreationConverter should only be used while deserializing.");
    }

    /// <summary>
    /// Reads the JSON representation of the object.
    /// </summary>
    /// <param name="reader">The <see cref="JsonReader"/> to read from.</param>
    /// <param name="objectType">Type of the object.</param>
    /// <param name="existingValue">The existing value of object being read.</param>
    /// <param name="serializer">The calling serializer.</param>
    /// <returns>The object value.</returns>
    public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
    {
        if (reader.TokenType == JsonToken.Null)
            return null;

        T value = CreateAndPopulate(objectType, serializer.Deserialize<Dictionary<String, String>>(reader));

        if (value == null)
            throw new JsonSerializationException("No object created.");

        return value;
    }

    /// <summary>
    /// Creates an object which will then be populated by the serializer.
    /// </summary>
    /// <param name="objectType">Type of the object.</param>
    /// <returns></returns>
    public abstract T CreateAndPopulate(Type objectType, Dictionary<String, String> jsonFields);

    /// <summary>
    /// Determines whether this instance can convert the specified object type.
    /// </summary>
    /// <param name="objectType">Type of the object.</param>
    /// <returns>
    ///     <c>true</c> if this instance can convert the specified object type; otherwise, <c>false</c>.
    /// </returns>
    public override bool CanConvert(Type objectType)
    {
        return typeof(T).IsAssignableFrom(objectType);
    }

    /// <summary>
    /// Gets a value indicating whether this <see cref="JsonConverter"/> can write JSON.
    /// </summary>
    /// <value>
    ///     <c>true</c> if this <see cref="JsonConverter"/> can write JSON; otherwise, <c>false</c>.
    /// </value>
    public override bool CanWrite
    {
        get
        {
            return false;
        }
    }
}

Next we create the factory class that will create our Foo:

public class FooFactory : FactoryConverter<Foo>
{
    public FooFactory(Bar bar)
    {
        this.Bar = bar;
    }

    public Bar Bar { get; private set; }

    public override Foo Create(Type objectType, Dictionary<string, string> arguments)
    {
        return new Foo(Bar, arguments["X"], arguments["Y"]);
    }
}

Here is sample code:

var bar = new Bar("BarObject");

var fooSrc = new Foo
(
    bar,
    "A", "B"
);

var str = JsonConvert.SerializeObject(fooSrc);

var foo = JsonConvert.DeserializeObject<Foo>(str, new FooFactory(bar));

Console.WriteLine(str);

In this case foo contains an argument that we needed to pass to a Foo constructor during deserialization.

Community
  • 1
  • 1
Lu4
  • 14,873
  • 15
  • 79
  • 132
10

I'm not an expert on Json.NET, but AFAIK that simply is not possible. If I were you, I would look at options to fixup this after deserialization.

Very few serialization APIs will allow you to control construction to that degree; the four most typical approaches are (most common first):

  • invoke the parameterless constructor
  • skip the constructor completely
  • use a constructor that has an obvious 1:1 mapping to the members being serialized
  • use a user-supplied factory method

It sounds like you want the last, which is pretty rare. You may have to settle for doing this outside the constructor.

Some serialization APIs offer "serialization/deserialization callbacks", which allow you to run a method on the object at various points (typically before and after both serialize and deserialize), including passing some context information into the callback. IF Json.NET supports deserialization callbacks, that might be the thing to look at. This question suggests that the [OnDeserialized] callback pattern may indeed be supported; the context comes from the JsonSerializerSettings's .Context property that you can optionally supply to the deserialize method.

Otherwise, just run it manually after deserialization.

My rough pseudo-code (completely untested):

// inside type: Foo
[OnDeserialized]
public void OnDeserialized(StreamingContext ctx) {
    if(ctx != null) {
        Bar bar = ctx.Context as Bar;
        if(bar != null) this.Bar = bar; 
    }
}

and

var ctx = new StreamingContext(StreamingContextStates.Other, bar);
var settings = new JsonSerializerSettings { Context = ctx };
var obj = JsonConvert.DeserializeObject<Foo>(fooJsonString, settings);
Community
  • 1
  • 1
Marc Gravell
  • 1,026,079
  • 266
  • 2,566
  • 2,900
  • 13
    No idea why serializer writers consider this low priority. I've always considered being able to construct immutable objects without low-level hack one of the really basic features a public interface based serializer should have. – CodesInChaos Nov 24 '11 at 09:39
  • 1
    @CodeInChaos protobuf-net supports all 4 options listed, *and* at least one other (surrogate transforms)... just sayin' – Marc Gravell Nov 24 '11 at 09:42
  • @CodeInChaos as for why: simply, that is really pretty hard – Marc Gravell Nov 24 '11 at 09:42
  • Marc thank you for your answer, I understand how to achieve the result, how to initialize object after it has been deserialized, as an alternative I could also use a singleton that contains Bar object, the problem is that both these approaches affects existing code base design which is pretty huge right now. Unfortunately if I use your solution then I will be forced to do huge amount of work changing/debugging existing codebase :( – Lu4 Nov 24 '11 at 09:56
  • @Lu4 adding a serialization-callback doesn't seem an overly odious task to me... but; up to you. I don't know of any way to do it directly, other than hacking the source-code of Json.NET, which I do not recommend. – Marc Gravell Nov 24 '11 at 10:02
  • I'm looking into an 'DeserializeObject<>' overload that accepts an array of JsonConverters, probably It is possible to wrap my arguments in CustomCreationConverter which will then create the Object I need... – Lu4 Nov 24 '11 at 10:09
  • FYI Json.NET does support non-default constructors. – James Newton-King Nov 27 '11 at 20:55
  • @JamesNewton-King even with a member unrelated to serialization? – Marc Gravell Nov 27 '11 at 21:35
  • @JamesNewton-King, hi James could you please look at my solution, I would like to hear your thought on how to make that approach more optimal. Thank you... – Lu4 Nov 29 '11 at 14:07
  • @JamesNewton-King does Json.NET support non-default constructors where you can pass something as as a ctor argument when deserializing? i.e Deserialize(json, settings, args); where args is an array of args to be passed to the instantiated class, so they can be set privately and not exposed? – Phill Dec 24 '14 at 09:03
  • Unfortunately there are no properties in SerializationContext at PCL especially a Context one. I got crazy in last days by trying to implement a common way to deserialize objects with external context. – Viacheslav Smityukh Jan 20 '16 at 09:40
0

If you have a constructor whose only parameters are your non-serialized values, create your instance first and then populate your object instead of deserializing. The JsonConvert class has a PopulateObject method, defined as follows:

public static void PopulateObject(
    string value,                      // JSON string
    object target)                     // already-created instance

If you have particular serialization settings, there's an overload that also includes a JsonSerializerSettings parameter.

Add a Foo constructor that has a single Bar parameter, you could do something like:

var bar = new Bar("Hello World");
var foo = new Foo(bar);
JsonConvert.PopulateObject(fooJsonString, foo);

You may need to adjust your class to use fields for mapping, or make adjustments to NHibernate to allow writing to private setters (use of a custom IProxyValidator class).

Remi Despres-Smyth
  • 4,173
  • 3
  • 36
  • 46