Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Token PropertyName in state Property would result in an invalid JSON object. when using custom JsonConverter<T>

I am trying to serialise/deserialise a .NET DataSet using Json.NET and a custom serialiser. I know many of you will tell me not to (I have seen this on other posts) I have a valid reason and wish to continue down this route.

My serialisation is based upon the fact that the .NET DataSet can export it's schema and data to XML and then re-import the same; on that basis, I am trying to create a converter that will allow me to capture that XML, convert it to JSON then convert it back and reload it. My implementation is as follows...

class DataSetConverter : JsonConverter<DataSet>
{
    public override DataSet ReadJson(JsonReader reader, Type objectType, DataSet existingValue, bool hasExistingValue, JsonSerializer serializer)
    {
        DataSet dataSet = new DataSet();
        JObject jObject = JObject.Load(reader);

        String json = jObject.ToString();
        XDocument document = JsonConvert.DeserializeXNode(json);
        using (MemoryStream memoryStream = new MemoryStream())
        using (StreamWriter streamWriter = new StreamWriter(memoryStream))
        {
            streamWriter.Write(document.ToString(SaveOptions.None));
            streamWriter.Flush();

            memoryStream.Position = 0;
            dataSet.ReadXml(memoryStream);
        }

        return dataSet;
    }

    public override void WriteJson(JsonWriter writer, DataSet dataSet, JsonSerializer serializer)
    {
        using (MemoryStream memoryStream = new MemoryStream())
        {
            dataSet.WriteXml(memoryStream, XmlWriteMode.WriteSchema);
            using (StreamReader reader = new StreamReader(memoryStream))
            {
                memoryStream.Seek(0, SeekOrigin.Begin);
                XDocument document = XDocument.Parse(reader.ReadToEnd());
                writer.WriteRaw(JsonConvert.SerializeXNode(document, Formatting.Indented, false));
            }
        }
    }
}

Used as follows (purely serialising a DataSet object) it works (my new DataSet has the same schema and data as the original)...

DataSet originalInserts = new DataSet("Inserts");
DataTable originalStuff = originalInserts.Tables.Add("Stuff");

originalStuff.Columns.Add("C1", typeof(String));
originalStuff.Columns.Add("C2", typeof(Int64));
originalStuff.Columns.Add("C3", typeof(Guid));
originalStuff.Columns.Add("C4", typeof(float));

originalStuff.Rows.Add("One", 2, Guid.NewGuid(), 4.4);

String json = JsonConvert.SerializeObject(originalInserts, Formatting.Indented, new DataSetConverter());

DataSet newInsertsFromConvertedXml = (DataSet)JsonConvert.DeserializeObject(json, typeof(DataSet), new DataSetConverter());

However if I then try and use the same converter with an object that contains a DataSet (the very same DataSet as above)...

public class TestClass
{
    public DataSet Inserts { get; set; }

    public String SomethingElse { get; set; }
}

TestClass testClass = new TestClass { Inserts = originalInserts, SomethingElse = "Me" };
json = JsonConvert.SerializeObject(testClass, Formatting.Indented, new DataSetConverter());

It fails with

Token PropertyName in state Property would result in an invalid JSON object. Path ''.

I have also tried decorating the DataSet on the TestClass with a JsonConverter attribute and removing the converter from the Serialize method call but get the same result...

public class TestClass
{
    [JsonConverter(typeof(DataSetConverter))]
    public DataSet Inserts { get; set; }

    public String SomethingElse { get; set; }
}

TestClass testClass = new TestClass { Inserts = originalInserts, SomethingElse = "Me" };
json = JsonConvert.SerializeObject(testClass, Formatting.Indented);

What am I missing?

like image 972
Martin Robins Avatar asked Jan 30 '19 13:01

Martin Robins


1 Answers

Your basic problem is that you should be calling WriteRawValue() instead of WriteRaw():

writer.WriteRawValue(JsonConvert.SerializeXNode(document, Formatting.Indented, false)); 

The documentation for WriteRawValue() states:

Writes raw JSON where a value is expected and updates the writer's state.

Whereas the documentation WriteRaw() states:

Writes raw JSON without changing the writer's state.

The failure to advance the writer's state explains why an exception is thrown when an attempt is made to write subsequent content.

That being said, you're creating a lot of unnecessary intermediate string, Stream and JObject representations inside your converter. A simpler approach would be, in WriteJson() to:

  1. Construct an XDocument and write the DataSet directly to it using XContainer.CreateWriter();

  2. Serialize the XDocument directly to the incoming JsonWriter by constructing a local XmlNodeConverter.

Serialization would follow the reverse process. Thus your DataSetConverter would look like:

class DataSetConverter : JsonConverter<DataSet>
{
    public override DataSet ReadJson(JsonReader reader, Type objectType, DataSet existingValue, bool hasExistingValue, JsonSerializer serializer)
    {
        if (reader.MoveToContent().TokenType == JsonToken.Null)
            return null;
        var converter = new XmlNodeConverter { OmitRootObject = false };
        var document = (XDocument)converter.ReadJson(reader, typeof(XDocument), existingValue, serializer);
        using (var xmlReader = document.CreateReader())
        {
            var dataSet = existingValue ?? new DataSet();
            dataSet.ReadXml(xmlReader);
            return dataSet;
        }
    }

    public override void WriteJson(JsonWriter writer, DataSet dataSet, JsonSerializer serializer)
    {
        var document = new XDocument();
        using (var xmlWriter = document.CreateWriter())
        {
            dataSet.WriteXml(xmlWriter, XmlWriteMode.WriteSchema);
        }
        var converter = new XmlNodeConverter { OmitRootObject = false };
        converter.WriteJson(writer, document, serializer);
    }
}

public static partial class JsonExtensions
{
    public static JsonReader MoveToContent(this JsonReader reader)
    {
        // Start up the reader if not already reading, and skip comments
        if (reader.TokenType == JsonToken.None)
            reader.Read();
        while (reader.TokenType == JsonToken.Comment && reader.Read())
            {}
        return reader;
    }
}

Notes:

  1. You are inheriting from JsonConverter<DataSet>, and in ReadJson() you construct an object of type DataSet directly. However, as shown in the reference source, JsonConverter<T>.CanConvert(Type objectType) applies to all subclasses of the type T also:

    public sealed override bool CanConvert(Type objectType)
    {
        return typeof(T).IsAssignableFrom(objectType);
    }
    

    Thus you may need to override CanConvert and make it apply only when the object type is equal to typeof(DataSet) -- but since the method has been sealed, you cannot. Therefore, it may prove necessary to inherit from the non-generic base class JsonConverter instead.

like image 122
dbc Avatar answered Nov 06 '22 05:11

dbc