12

I am simply trying to serialize and deserialize a string array in Bson format using Json.NET, but the following code fails:

var jsonSerializer = new JsonSerializer();
var array = new string [] { "A", "B" };

// Serialization
byte[] bytes;
using (var ms = new MemoryStream())
using (var bson = new BsonWriter(ms))
{
    jsonSerializer.Serialize(bson, array, typeof(string[]));
    bytes = ms.ToArray();
}

// Deserialization
using (var ms = new MemoryStream(bytes))
using (var bson = new BsonReader(ms))
{
    // Exception here
    array = jsonSerializer.Deserialize<string[]>(bson);
}

Exception message:

Cannot deserialize the current JSON object (e.g. {"name":"value"}) into type 'System.String[]' because the type requires a JSON array (e.g. [1,2,3]) to deserialize correctly.

To fix this error either change the JSON to a JSON array (e.g. [1,2,3]) or change the deserialized type so that it is a normal .NET type (e.g. not a primitive type like integer, not a collection type like an array or List) that can be deserialized from a JSON object. JsonObjectAttribute can also be added to the type to force it to deserialize from a JSON object.

How can I get this to work?

Community
  • 1
  • 1
Xavier Poinas
  • 19,377
  • 14
  • 63
  • 95

5 Answers5

19

Set ReadRootValueAsArray to true on BsonReader

http://james.newtonking.com/projects/json/help/index.html?topic=html/P_Newtonsoft_Json_Bson_BsonReader_ReadRootValueAsArray.htm

This setting is required because the BSON data spec doesn't save metadata about whether the root value is an object or an array.

James Newton-King
  • 48,174
  • 24
  • 109
  • 130
6

Hmmm, from where I sit, your code should work, but Json.Net seems to think that your serialized array of strings is a dictionary. This could be because, according to the BSON specification, arrays actually do get serialized as a list of key-value pairs just like objects do. The keys in this case are simply the string representations of the array index values.

In any case, I was able to work around the issue in a couple of different ways:

  1. Deserialize to a Dictionary and then manually convert it back to an array.

    var jsonSerializer = new JsonSerializer();
    var array = new string[] { "A", "B" };
    
    // Serialization
    byte[] bytes;
    using (var ms = new MemoryStream())
    using (var bson = new BsonWriter(ms))
    {
        jsonSerializer.Serialize(bson, array);
        bytes = ms.ToArray();
    }
    
    // Deserialization
    using (var ms = new MemoryStream(bytes))
    using (var bson = new BsonReader(ms))
    {
        var dict = jsonSerializer.Deserialize<Dictionary<string, string>>(bson);
        array = dict.OrderBy(kvp => kvp.Key).Select(kvp => kvp.Value).ToArray();
    }
    
  2. Wrap the array in an outer object.

    class Wrapper
    {
        public string[] Array { get; set; }
    }
    

    Then serialize and deserialize using the wrapper object.

    var jsonSerializer = new JsonSerializer();
    var obj = new Wrapper { Array = new string[] { "A", "B" } };
    
    // Serialization
    byte[] bytes;
    using (var ms = new MemoryStream())
    using (var bson = new BsonWriter(ms))
    {
        jsonSerializer.Serialize(bson, obj);
        bytes = ms.ToArray();
    }
    
    // Deserialization
    using (var ms = new MemoryStream(bytes))
    using (var bson = new BsonReader(ms))
    {
        obj = jsonSerializer.Deserialize<Wrapper>(bson);
    }
    

Hope this helps.

Brian Rogers
  • 125,747
  • 31
  • 299
  • 300
  • Thanks or the analysis and possible work-arounds. In my case though they are a bit hard to implement as I need this code to work with unknown types. I have opened an issue on the CodePlex website: https://json.codeplex.com/workitem/24365 – Xavier Poinas Jun 05 '13 at 02:40
  • 3
    @XavierPoinas The second solution, wrapping the array in an outer object, works great for unknown types. Simply change the property type of the wrapper class, i.e. `public object Value { get; set; }` instead of `public string[] Array { get; set; }`, and you can use it with any non-array types as well. It's easy to create a helper class that does the wrapping/unwrapping automatically. – Livven Jan 29 '15 at 00:13
2

As explained in this answer by James Newton-King, the BSON format doesn't save metadata about whether the root value is a collection, making it necessary to set BsonDataReader.ReadRootValueAsArray appropriately before beginning to deserialize.

One easy way to do this, when deserializing to some known POCO type (rather than dynamic or JToken), is to initialize the reader based on whether the root type will be serialized using an array contract. The following extension methods do this:

public static partial class BsonExtensions
{
    public static T DeserializeFromFile<T>(string path, JsonSerializerSettings settings = null)
    {
        using (var stream = new FileStream(path, FileMode.Open))
            return Deserialize<T>(stream, settings);
    }

    public static T Deserialize<T>(byte [] data, JsonSerializerSettings settings = null)
    {
        using (var stream = new MemoryStream(data))
            return Deserialize<T>(stream, settings);
    }

    public static T Deserialize<T>(byte [] data, int index, int count, JsonSerializerSettings settings = null)
    {
        using (var stream = new MemoryStream(data, index, count))
            return Deserialize<T>(stream, settings);
    }

    public static T Deserialize<T>(Stream stream, JsonSerializerSettings settings = null)
    {
        // Use BsonReader in Json.NET 9 and earlier.
        using (var reader = new BsonDataReader(stream) { CloseInput = false })  // Let caller dispose the stream
        {
            var serializer = JsonSerializer.CreateDefault(settings);
            //https://www.newtonsoft.com/json/help/html/DeserializeFromBsonCollection.htm
            if (serializer.ContractResolver.ResolveContract(typeof(T)) is JsonArrayContract)
                reader.ReadRootValueAsArray = true;
            return serializer.Deserialize<T>(reader);
        }
    }
}

Now you can simply do:

var newArray = BsonExtensions.Deserialize<string []>(bytes);

Notes:

  • BSON support was moved to its own package, Newtonsoft.Json.Bson, in Json.NET 10.0.1. In this version and later versions BsonDataReader replaces the now-obsolete BsonReader.

  • The same extension methods can be used to deserialize a dictionary, e.g.:

    var newDictionary = BsonExtensions.Deserialize<SortedDictionary<int, string>>(bytes);
    

    By checking the contract type ReadRootValueAsArray is set appropriately.

Demo fiddle here.

dbc
  • 104,963
  • 20
  • 228
  • 340
1

In general, you could check data type first before set ReadRootValueAsArray to true, like this:

if (typeof(IEnumerable).IsAssignableFrom(type)) 
    bSonReader.ReadRootValueAsArray = true;
Minh Nguyen
  • 2,106
  • 1
  • 28
  • 34
1

I know this is an old thread but I discovered a easy deserialization while using the power of MongoDB.Driver

You can use BsonDocument.parse(JSONString) to deserialize a JSON object so to deserialize a string array use this:

string Jsonarray = "[\"value1\", \"value2\", \"value3\"]";
BsonArray deserializedArray = BsonDocument.parse("{\"arr\":" + Jsonarray + "}")["arr"].asBsonArray;

deserializedArray can then be used as any array such as a foreach loop.

Frank Cedeno
  • 183
  • 2
  • 7