2

I'm getting a really strange situation where I'm trying to serialize an object returned by a third party API into JSON. I don't have any control over the third party API or the object it returns. The C# POCO I'm trying to serialize looks something like this:

public class JobSummary {
    public Job Job { get; set; }    
}

public class Job {
    public Status Status { get; set; }
}

public class Status {
    public object JobOutput { get; set; }
    public int Progress { get; set; }
}

Based on what the third party library returns, I would expect it to serialize to the following. At runtime, I can tell that the type of JobOutput is a JObject that contains a single key (Count) and value (0).

{
   job: {
       status: {
           jobOutput: {
               Count: 0
           },
           progress: 100
       }
   }
}

In this, job and status are obviously objects. progress is an int and jobOutput is a JObject.

If I run any of the following variations:

  1. JToken.FromObject(jobSummary)
  2. JObject.FromObject(jobSummary)
  3. JObject.Parse(jobSummary)

And ToString() or JsonConvert.SerializeObject() the result, I get the following output:

{
   job: {
       status: {
           jobOutput: {
               Count: []
           },
           progress: 100
       }
   }
}

Notice that Count has become an [].

But if I do jobSummary.Status.JobOutput.ToString(), I correctly get back 0, so I know that the POCO returned by the third party library isn't malformed and has the info I need.

Does anybody know what could be going on? Or how I can correctly serialize the nested JObject?

Edit: I should clarify that I'm on v6.0.8 of Newtonsoft for reasons outside my control, and that the thirdparty assembly that contains the POCO has an unknown version of Newtonsoft ILMerged in it. I don't know if that is relevant.

khalid13
  • 2,767
  • 2
  • 30
  • 48
  • if the problem is created by the version, you can use the latest version and redirect to old one in the ``.conf``:```` – Mohammed Sajid Apr 02 '20 at 16:12
  • @sajid unfortunately that's not possible due to the architecture of the greater application I'm working in. :( Plus I'm not even sure the problem is created by the version. – khalid13 Apr 02 '20 at 16:14
  • ok, do you have any example for the ``object`` to serialize? – Mohammed Sajid Apr 02 '20 at 16:25
  • @Sajid the first JSON in my question is representative of the object I'm trying to serialize. – khalid13 Apr 02 '20 at 16:33
  • 2
    we need to see the "POCO" in questions.... otherwise how can we figure out what is happening? – Jonathan Alfaro Apr 02 '20 at 16:35
  • if i try this object, it's work well : ``dynamic dynamicStuff = new { job = new { status = new { jobOutput = new { Count = 0 }, progress = 100 } } };``then like @JonathanAlfaro say, we need the POCO . – Mohammed Sajid Apr 02 '20 at 16:37
  • @JonathanAlfaro I added in the POCO. – khalid13 Apr 02 '20 at 17:04
  • It seems like the DLLs actually used for your call to `JsonConvert` and to construct the `JObject` value for `JobOutput` may not match What happens if you replace the `JObject` value for `JobOutput` with `new JRaw(jobOutput.ToString())`? – dbc Apr 02 '20 at 18:51
  • @dbc I suspect that would work... but the code would be fragile since I would be relying on the constituent parts of a JobSummary to stay the same. Since I don't control the makeup of a JobSummary, I can't rely on that. – khalid13 Apr 02 '20 at 19:29
  • Well, first check if it works. Then we can think how to make it robust, e.g. by adding a custom `JsonConverter` for "foreign" versions of Json.NET objects. – dbc Apr 02 '20 at 19:35
  • @dbc I've checked jobOutput.ToString() and it works. The custom converter solutions sounds like the right way to go... I'm exploring that right now but haven't come to an answer yet. – khalid13 Apr 02 '20 at 20:00

1 Answers1

3

You wrote that

I should clarify that I'm on v6.0.8 of Newtonsoft for reasons outside my control, and that the thirdparty assembly that contains the POCO has an unknown version of Newtonsoft ILMerged in it.

This explains your problem. The JobOutput contains an object with full name Newtonsoft.Json.Linq.JObject from a completely different Json.NET DLL than the one you are using. When your version of Json.NET tests to see whether the object being serialized is a JToken, it checks objectType.IsSubclassOf(typeof(JToken)) -- which will fail since the ILMerged type is not, in fact, a subclass of your version's type, despite having the same name.

As a workaround, you will need to create custom JsonConverter logic that uses the ToString() methods of the foreign JToken objects to generate output JSON, then writes that JSON to the JSON stream you are generating. The following should do the job:

public class ForeignJsonNetContainerConverter : ForeignJsonNetBaseConverter
{
    static readonly string [] Names = new []
    {
        "Newtonsoft.Json.Linq.JObject",
        "Newtonsoft.Json.Linq.JArray",
        "Newtonsoft.Json.Linq.JConstructor",
        "Newtonsoft.Json.Linq.JRaw",
    };

    protected override IReadOnlyCollection<string> TypeNames { get { return Names; } }

    public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
    {
        var json = value.ToString();
        // Fix indentation
        using (var stringReader = new StringReader(json))
        using (var jsonReader = new JsonTextReader(stringReader))
        {
            writer.WriteToken(jsonReader);
        }
    }
}

public class ForeignJsonNetValueConverter : ForeignJsonNetBaseConverter
{
    static readonly string [] Names = new []
    {
        "Newtonsoft.Json.Linq.JValue",
    };

    protected override IReadOnlyCollection<string> TypeNames { get { return Names; } }

    public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
    {
        var underlyingValue = ((dynamic)value).Value;
        if (underlyingValue == null)
        {
            writer.WriteNull();
        }
        else
        {
            // JValue.ToString() will be wrong for DateTime objects, we need to serialize them instead.
            serializer.Serialize(writer, underlyingValue);
        }
    }
}

public abstract class ForeignJsonNetBaseConverter : JsonConverter
{
    protected abstract IReadOnlyCollection<string> TypeNames { get; }

    public override bool CanConvert(Type objectType)
    {
        if (objectType.IsPrimitive)
            return false;
        // Do not use the converter for Native JToken types, only non-native types with the same name(s).
        if (objectType == typeof(JToken) || objectType.IsSubclassOf(typeof(JToken)))
            return false;
        var fullname = objectType.FullName;
        if (TypeNames.Contains(fullname))
            return true;
        return false;
    }

    public override bool CanRead { get { return false; } }

    public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
    {
        throw new NotImplementedException();
    }
}

And then use them in settings as follows:

var settings = new JsonSerializerSettings
{
    Converters = 
    {
        new ForeignJsonNetContainerConverter(), new ForeignJsonNetValueConverter()
    },
};

var json = JsonConvert.SerializeObject(summary, Formatting.Indented, settings);

Notes:

  • The converters work by assuming that types whose FullName matches a Json.NET type's name are, in fact, Json.NET types from a different version.

  • JValue.ToString() returns localized values for DateTime objects (see here for details), so I created a separate converter for JValue.

  • I also fixed the indentation to match.

Mockup fiddle here.

dbc
  • 104,963
  • 20
  • 228
  • 340
  • I ended up coming to a similar solution separately, but yours is much more thorough and generalizable. This is exactly what the issue ended up being, thank you! – khalid13 Apr 02 '20 at 22:40