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.

11
  • if the problem is created by the version, you can use the latest version and redirect to old one in the .conf:<bindingRedirect oldVersion="0.0.0.0-12.0.0.0" newVersion="6.0.0.0" /> Commented Apr 2, 2020 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. Commented Apr 2, 2020 at 16:14
  • ok, do you have any example for the object to serialize? Commented Apr 2, 2020 at 16:25
  • @Sajid the first JSON in my question is representative of the object I'm trying to serialize. Commented Apr 2, 2020 at 16:33
  • 2
    we need to see the "POCO" in questions.... otherwise how can we figure out what is happening? Commented Apr 2, 2020 at 16:35

1 Answer 1

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.

Sign up to request clarification or add additional context in comments.

1 Comment

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!

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.