views:

1056

answers:

4

I'm working with the DataContractJsonSerializer in Silverlight 4 and would like to deserialize the following JSON:

{
    "collectionname":"Books",
    "collectionitems": [
            ["12345-67890",201,
             "Book One"],
            ["09876-54321",45,
             "Book Two"]
        ]
}

Into classes like the following:

class BookCollection
{
  public string collectionname { get; set; }
  public List<Book> collectionitems { get; set; }
}

class Book
{
  public string Id { get; set; }
  public int NumberOfPages { get; set; }
  public string Title { get; set; }
}

What's the proper place to extend DataContractJsonSerializer to map the unnamed first array element in "collectionitems" to the Id property of the Book class, the second element to the NumberOfPages property and the final element to Title? I don't have control over the JSON generation in this instance and would like the solution to work with the Silverlight subset of .NET. It would be great if the solution could perform the reverse for serialization as well.

+3  A: 

If this weren't Silverlight, you could use IDataContractSurrogate to use object[] (what's actually present in your JSON) instead of Book when serializing/deserializing. Sadly, IDataContractSurrogate (and the overloads of the DataContractJsonSerializer constructor which use it) aren't available in Silverlight.

On Silverlight, here's a hacky but simple workaround. Derive the Book class from a type which imlpements ICollection<object>. Since the type in your serialized JSON is object[], the framework will dutifully serialize it into your ICollection<object>, which in turn you can wrap with your properties.

The easiest (and hackiest) is just to derive from List<object>. This easy hack has the downside that users can modify the underlying list data and mess up your properties. If you're the only user of this code, that might be OK. With a little more work, you can roll your own implementation of ICollection and permit only enough methods to run for serialization to work, and throwing exceptions for the rest. I included code samples for both approaches below.

If the above hacks are too ugly for you, I'm sure there are more graceful ways to handle this. You'd probably want to focus your attention on creating a custom collection type instead of List<Book> for your collectionitems property. This type could contain a field of type List<object[]> (which is the actual type in your JSON) which you might be able to convince the serializer to populate. Then your IList implementation could mine that data into actual Book instances.

Another line of investigation could try casting.For example could you implement an implicit type conversion between Book and string[] and would serialization be smart enough to use it? I doubt it, but it may be worth a try.

Anyway, here's code samples for the derive-from-ICollection hacks noted above. Caveat: I haven't verified these on Silverlight, but they should be using only Silverlight-accessible types so I think (fingers crossed!) it should work OK.

Easy, Hackier Sample

using System;
using System.Collections.Generic;
using System.Text;
using System.Runtime.Serialization.Json;
using System.Runtime.Serialization;
using System.IO;

[DataContract]
class BookCollection
{
    [DataMember(Order=1)]
    public string collectionname { get; set; }

    [DataMember(Order = 2)]
    public List<Book> collectionitems { get; set; }
}

[CollectionDataContract]
class Book : List<object>
{
    public string Id { get { return (string)this[0]; } set { this[0] = value; } }
    public int NumberOfPages { get { return (int)this[1]; } set { this[1] = value; } }
    public string Title { get { return (string)this[2]; } set { this[2] = value; } }

}

class Program
{
    static void Main(string[] args)
    {
        DataContractJsonSerializer ser = new DataContractJsonSerializer(typeof(BookCollection));
        string json = "{"
                    + "\"collectionname\":\"Books\","
                    + "\"collectionitems\": [ "
                            + "[\"12345-67890\",201,\"Book One\"],"
                            + "[\"09876-54321\",45,\"Book Two\"]"
                        + "]"
                    + "}";

        using (MemoryStream ms = new MemoryStream(Encoding.Unicode.GetBytes(json)))
        {
            BookCollection obj = ser.ReadObject(ms) as BookCollection;
            using (MemoryStream ms2 = new MemoryStream())
            {
                ser.WriteObject(ms2, obj);
                string serializedJson = Encoding.UTF8.GetString(ms2.GetBuffer(), 0,  (int)ms2.Length);
            }
        }
    }
}

Harder, slightly-less-hacky sample

Here's the second sample, showing a manual implementation of ICollection, which prevents users from accessing the collection-- it supports calling Add() 3 times (during deserialization) but otherwise doesn't allow modification via ICollection<T>. The ICollection methods are exposed using explicit interface implementation and there are attributes on those methods to hide them from intellisense, which should further reduce the hack factor. But as you can see this is a lot more code.

using System;
using System.Collections.Generic;
using System.Text;
using System.Runtime.Serialization.Json;
using System.Runtime.Serialization;
using System.IO;
using System.Diagnostics;
using System.ComponentModel;

[DataContract]
class BookCollection
{
    [DataMember(Order=1)]
    public string collectionname { get; set; }

    [DataMember(Order = 2)]
    public List<Book> collectionitems { get; set; }
}

[CollectionDataContract]
class Book : ICollection<object>
{
    public string Id { get; set; }
    public int NumberOfPages { get; set; }
    public string Title { get; set; }

    // code below here is only used for serialization/deserialization

    // keeps track of how many properties have been initialized
    [EditorBrowsable(EditorBrowsableState.Never)]
    private int counter = 0;

    [EditorBrowsable(EditorBrowsableState.Never)]
    public void Add(object item)
    {
        switch (++counter)
        {
            case 1:
                Id = (string)item;
                break;
            case 2:
                NumberOfPages = (int)item;
                break;
            case 3:
                Title = (string)item;
                break;
            default:
                throw new NotSupportedException();
        }
    }

    [EditorBrowsable(EditorBrowsableState.Never)]
    IEnumerator<object> System.Collections.Generic.IEnumerable<object>.GetEnumerator() 
    {
        return new List<object> { Id, NumberOfPages, Title }.GetEnumerator();
    }

    [EditorBrowsable(EditorBrowsableState.Never)]
    System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() 
    {
        return new object[] { Id, NumberOfPages, Title }.GetEnumerator();
    }

    [EditorBrowsable(EditorBrowsableState.Never)]
    int System.Collections.Generic.ICollection<object>.Count 
    { 
        get { return 3; } 
    }

    [EditorBrowsable(EditorBrowsableState.Never)]
    bool System.Collections.Generic.ICollection<object>.IsReadOnly 
    { get { throw new NotSupportedException(); } }

    [EditorBrowsable(EditorBrowsableState.Never)]
    void System.Collections.Generic.ICollection<object>.Clear() 
    { throw new NotSupportedException(); }

    [EditorBrowsable(EditorBrowsableState.Never)]
    bool System.Collections.Generic.ICollection<object>.Contains(object item) 
    { throw new NotSupportedException(); }

    [EditorBrowsable(EditorBrowsableState.Never)]
    void System.Collections.Generic.ICollection<object>.CopyTo(object[] array, int arrayIndex) 
    { throw new NotSupportedException(); }

    [EditorBrowsable(EditorBrowsableState.Never)]
    bool System.Collections.Generic.ICollection<object>.Remove(object item) 
    { throw new NotSupportedException(); }
}

class Program
{
    static void Main(string[] args)
    {
        DataContractJsonSerializer ser = new DataContractJsonSerializer(typeof(BookCollection));
        string json = "{"
                    + "\"collectionname\":\"Books\","
                    + "\"collectionitems\": [ "
                            + "[\"12345-67890\",201,\"Book One\"],"
                            + "[\"09876-54321\",45,\"Book Two\"]"
                        + "]"
                    + "}";

        using (MemoryStream ms = new MemoryStream(Encoding.Unicode.GetBytes(json)))
        {
            BookCollection obj = ser.ReadObject(ms) as BookCollection;
            using (MemoryStream ms2 = new MemoryStream())
            {
                ser.WriteObject(ms2, obj);
                string serializedJson = Encoding.UTF8.GetString(ms2.GetBuffer(), 0,  (int)ms2.Length);
            }
        }
    }
}

BTW, the first time I read your quesiton I skipped over the important Silverlight requirement. Oops! Anyway, if not using Silverlight, here's the solution I coded up for that case-- it's much easier and I might as well save it here for any Googlers coming later.

The (on regular .NET framework, not Silverlight) magic you're looking for is IDataContractSurrogate. Implement this interface when you want to substitute one type for another type when serializing/deserializing. In your case you wnat to substitute object[] for Book.

Here's some code showing how this works:

using System;
using System.Collections.Generic;
using System.Text;
using System.Runtime.Serialization.Json;
using System.Runtime.Serialization;
using System.IO;
using System.Collections.ObjectModel;

[DataContract]
class BookCollection
{
    [DataMember(Order=1)]
    public string collectionname { get; set; }

    [DataMember(Order = 2)]
    public List<Book> collectionitems { get; set; }
}

class Book 
{ 
  public string Id { get; set; } 
  public int NumberOfPages { get; set; } 
  public string Title { get; set; } 
} 

// A type surrogate substitutes object[] for Book when serializing/deserializing.
class BookTypeSurrogate : IDataContractSurrogate
{
    public Type GetDataContractType(Type type)
    {
        // "Book" will be serialized as an object array
        // This method is called during serialization, deserialization, and schema export. 
        if (typeof(Book).IsAssignableFrom(type))
        {
            return typeof(object[]);
        }
        return type;
    }
    public object GetObjectToSerialize(object obj, Type targetType)
    {
        // This method is called on serialization.
        if (obj is Book)
        {
            Book book = (Book) obj;
            return new object[] { book.Id, book.NumberOfPages, book.Title };
        }
        return obj;
    }
    public object GetDeserializedObject(object obj, Type targetType)
    {
        // This method is called on deserialization.
        if (obj is object[])
        {
            object[] arr = (object[])obj;
            Book book = new Book { Id = (string)arr[0], NumberOfPages = (int)arr[1], Title = (string)arr[2] };
            return book;
        }
        return obj;
    }
    public Type GetReferencedTypeOnImport(string typeName, string typeNamespace, object customData)
    {
        return null; // not used
    }
    public System.CodeDom.CodeTypeDeclaration ProcessImportedType(System.CodeDom.CodeTypeDeclaration typeDeclaration, System.CodeDom.CodeCompileUnit compileUnit)
    {
        return typeDeclaration; // Not used
    }
    public object GetCustomDataToExport(Type clrType, Type dataContractType)
    {
        return null; // not used
    }
    public object GetCustomDataToExport(System.Reflection.MemberInfo memberInfo, Type dataContractType)
    {
        return null; // not used
    }
    public void GetKnownCustomDataTypes(Collection<Type> customDataTypes)
    {
        return; // not used
    }
}


class Program
{
    static void Main(string[] args)
    {
        DataContractJsonSerializer ser  =
            new DataContractJsonSerializer(
                typeof(BookCollection), 
                new List<Type>(),        /* knownTypes */
                int.MaxValue,            /* maxItemsInObjectGraph */ 
                false,                   /* ignoreExtensionDataObject */
                new BookTypeSurrogate(),  /* dataContractSurrogate */
                false                    /* alwaysEmitTypeInformation */
                );
        string json = "{"
                    + "\"collectionname\":\"Books\","
                    + "\"collectionitems\": [ "
                            + "[\"12345-67890\",201,\"Book One\"],"
                            + "[\"09876-54321\",45,\"Book Two\"]"
                        + "]"
                    + "}";

        using (MemoryStream ms = new MemoryStream(Encoding.Unicode.GetBytes(json)))
        {
            BookCollection obj = ser.ReadObject(ms) as BookCollection;
            using (MemoryStream ms2 = new MemoryStream())
            {
                ser.WriteObject(ms2, obj);
                string serializedJson = Encoding.UTF8.GetString(ms2.GetBuffer(), 0,  (int)ms2.Length);
            }
        }
    }
}
Justin Grant
Very interesting solutions! I like it! Two small remarks only: 1) one should add constructor `public Book () : base (3) { base.Add (null); base.Add (0); base.Add (null); return;}` to the `Book` class of the first solution or one receive `ArgumentOutOfRangeException` in setter. 2) It's a little pity, that the data in `Book` class will be hold as untyped List<object> and strong typed version will be used only for serialization/deserialization. Can one change this in the first solution?
Oleg
+1: Second version is very elegant. This example is now in the list of my favorites.
Oleg
Hi Oleg - thanks for the suggestions! Unfortunately the `List<T>(int)` constructor doesn't set a limit, it sets a starting capacity, so nothing would prevent growing the list later. But check out my new code sample showing an manual `ICollection<object>` implementation. It's cleaner and safer, at cost of much more code.
Justin Grant
Excellent solutions, thanks for taking the time to show multiple alternatives!
James Cadd
+1  A: 

I find your question very interesting. So I have to spent my time in trying to solve the problem. Currently I received an example which can serialize and deserealize JSON data like following:

{
  "collectionname":"Books",
  "collectionitems":[
    {"book":["12345-67890",201,"Book One"]},
    {"book":["09876-54321",45,"Book Two"]}
  ]
}

the corresponding code of a small console application:

using System;
using System.Collections.Generic;
using System.IO;
using System.Runtime.Serialization;
using System.Security.Permissions;

namespace DataContractJsonSerializer {
    [DataContract]
    class BookCollection {
        [DataMember (Order = 0)]
        public string collectionname { get; set; }
        [DataMember (Order = 1)]
        public List<Book> collectionitems { get; set; }
    }

    [Serializable]
    [KnownType (typeof (object[]))]
    class Book: ISerializable {
        public string Id { get; set; }
        public int NumberOfPages { get; set; }
        public string Title { get; set; }

        public Book () { }

        [SecurityPermissionAttribute (SecurityAction.Demand, Flags = SecurityPermissionFlag.SerializationFormatter)]
        protected Book (SerializationInfo info, StreamingContext context) {
            // called by DataContractJsonSerializer.ReadObject
            Object[] ar = (Object[]) info.GetValue ("book", typeof (object[]));

            this.Id = (string)ar[0];
            this.NumberOfPages = (int)ar[1];
            this.Title = (string)ar[2];
        }

        [SecurityPermission (SecurityAction.Demand, SerializationFormatter = true)]
        public void GetObjectData (SerializationInfo info, StreamingContext context) {
            // called by DataContractJsonSerializer.WriteObject
            object[] ar = new object[] { (object)this.Id, (object)this.NumberOfPages, (object)this.Title };
            info.AddValue ("book", ar);
        }
    }

    class Program {
        static readonly string testJSONdata = "{\"collectionname\":\"Books\",\"collectionitems\":[{\"book\":[\"12345-67890\",201,\"Book One\"]},{\"book\":[\"09876-54321\",45,\"Book Two\"]}]}";

        static void Main (string[] args) {
            BookCollection test = new BookCollection () {
                collectionname = "Books",
                collectionitems = new List<Book> {
                    new Book() { Id = "12345-67890", NumberOfPages = 201, Title = "Book One"},
                    new Book() { Id = "09876-54321", NumberOfPages = 45, Title = "Book Two"},
                }
            };

            MemoryStream memoryStream = new MemoryStream ();
            System.Runtime.Serialization.Json.DataContractJsonSerializer ser =
                new System.Runtime.Serialization.Json.DataContractJsonSerializer (typeof (BookCollection));
            memoryStream.Position = 0;
            ser.WriteObject (memoryStream, test);

            memoryStream.Flush();
            memoryStream.Position = 0;
            StreamReader sr = new StreamReader(memoryStream);
            string str = sr.ReadToEnd ();
            Console.WriteLine ("The result of custom serialization:");
            Console.WriteLine (str);

            if (String.Compare (testJSONdata, str, StringComparison.Ordinal) != 0) {
                Console.WriteLine ("Error in serialization: unexpected results.");
                    return;
            }

            byte[] jsonDataAsBytes = System.Text.Encoding.GetEncoding ("iso-8859-1").GetBytes (testJSONdata);
            MemoryStream stream = new MemoryStream (jsonDataAsBytes);
            stream.Position = 0;
            BookCollection p2 = (BookCollection)ser.ReadObject (stream);
        }
    }
}

I don't yet tested this approach under Silverlight 4.

Oleg
Unfortunately, apparently the [Serializable] attribute doesn't work in Silverlight. Quoting from http://blogs.msdn.com/suwatch/archive/2009/01/21/wcf-silverlight-exception-and-serialization.aspx, "in Silverlight, there is no ISerializable interface".
Justin Grant
Also, another problem with your solution is that the JSON doesn't match the OP's example-- and the OP said that he can't change the incoming JSON format, he's stuck with what's there now. His arrays are anonymous (no "book" name attached to each array) so your serialization example won't work with his JSON, even in non-Silverlight .NET.
Justin Grant
Also, `[SecurityPermission (SecurityAction.Demand, SerializationFormatter = true` doesn't do anything on Silverlight and is marked [Obsolete]. Per http://msdn.microsoft.com/en-us/library/system.security.permissions.securitypermissionattribute(VS.95).aspx, "You can use this class in a Silverlight-based application, but it will have no effect."
Justin Grant
A: 

Hi

Great posting.

What is the purpose of the internally scoped MemoryStream ms2?

using (MemoryStream ms = new MemoryStream(Encoding.Unicode.GetBytes(json))) 
        { 
            BookCollection obj = ser.ReadObject(ms) as BookCollection; 
            using (MemoryStream ms2 = new MemoryStream()) 
            { 
                ser.WriteObject(ms2, obj); 
                string serializedJson = Encoding.UTF8.GetString(ms2.GetBuffer(), 0,  (int)ms2.Length); 
            } 
        } 
jeff00seattle
A: 

Person Class

[DataContract]
public class Person
{
    [DataMember]
    public string Name;
    [DataMember]
    public int Age;
}

Routine sample

class Program
{
    static void Main(string[] args)
    {
        Collection<Person> persons = new Collection<Person>();
        Person person1 = new Person();
        person1.Name = "Fist Person";
        person1.Age = 31;
        persons.Add(person1);
        Person person2 = new Person();
        person2.Name = "Second Person";
        person2.Age = 30;
        persons.Add(person2);
        MemoryStream stream1 = new MemoryStream();
        DataContractJsonSerializer ser = new DataContractJsonSerializer(typeof(Collection<Person>));
        ser.WriteObject(stream1, persons);
        stream1.Position = 0;
        StreamReader sr = new StreamReader(stream1);
        Console.Write("JSON conversion from Person collection object: ");
        Console.WriteLine(sr.ReadToEnd());
        Console.ReadKey();
        Console.Clear();
    }
}
ASP Force