Reputation: 73
I have a requirement where I need a property that is actually a JSON value from a MongoDB collection that needs to be deserialized into a string. This conversion is throwing a "Cannot deserialize a 'String' from a BsonType 'Document'" exception.
I tried implementing a JSON custom converter, but as the value is being treated as a BsonDocument, it is not helping and I am getting the same exception. I also need it in the original format as I need to cast it back into a BsonDocument down the line. I guess I would need a custom Bson serializer/deserializer.
Incoming sample document from MongoDB collection:
{
"name": "Jane Doe",
"dob": {
"month": "Sep",
"day": 09,
"year": 1987
}
}
Type it is expecting for deserialization:
public class Person
{
public string name { get; set; }
public Dob dob { get; set; }
public class Dob
{
public string month { get; set; }
public int day { get; set; }
public int year { get; set; }
}
}
Type I want it to deserialize into:
public class Person
{
public string name { get; set; }
public string dob { get; set; }
}
Upvotes: 3
Views: 6523
Reputation: 116532
To summarize, you have a public-facing string
property on your model that contains JSON which you would like to internally serialize to MongoDB by deserializing the JSON string to some intermediate DTO, then serializing the DTO itself to Mongo.
Here are a couple of approaches to solving your problem.
Firstly, you could introduce a private DTO-valued property Dob SerializedDOB { get; set; }
into your data model, mark that property with [BsonElement("dob")]
to force it to be serialized, then modify dob
to be a non-serialized surrogate property that serializes from and to the underlying SerializedDOB
within its getter and setter. The following code shows this approach:
public class Person
{
public string name { get; set; }
[BsonIgnore]
public string dob
{
get => BsonExtensionMethods.ToJson(SerializedDOB);
set => SerializedDOB = MyBsonExtensionMethods.FromJson<Dob>(value);
}
[BsonElement("dob")]
Dob SerializedDOB { get; set; }
class Dob // The DTO
{
public string month { get; set; }
public int day { get; set; }
public int year { get; set; }
}
}
The advantage of this approach is that, by making the JSON string a surrogate, the setter automatically ensures that it is well-formed.
Demo fiddle #1 here.
Secondly, you could create a custom SerializerBase<string>
for dob
that maps the string value to and from the DTO Dob
during (de)serialization. The following code shows this approach:
public class Person
{
public string name { get; set; }
[BsonSerializer(typeof(JsonStringAsObjectSerializer<Dob>))]
public string dob { get; set; }
class Dob // The DTO
{
public string month { get; set; }
public int day { get; set; }
public int year { get; set; }
}
}
public class JsonStringAsObjectSerializer<TObject> : SerializerBase<string> where TObject : class
{
public override void Serialize(BsonSerializationContext context, BsonSerializationArgs args, string value)
{
if (value == null)
{
var bsonWriter = context.Writer;
bsonWriter.WriteNull();
}
else
{
var obj = MyBsonExtensionMethods.FromJson<TObject>(value);
var serializer = BsonSerializer.LookupSerializer(typeof(TObject));
serializer.Serialize(context, obj);
}
}
public override string Deserialize(BsonDeserializationContext context, BsonDeserializationArgs args)
{
var bsonReader = context.Reader;
var serializer = BsonSerializer.LookupSerializer(typeof(TObject));
var obj = (TObject)serializer.Deserialize(context);
return (obj == null ? null : BsonExtensionMethods.ToJson(obj));
}
}
The advantage of this approach is that JsonStringAsObjectSerializer<TObject>
can be reused whenever this requirement arises.
Demo fiddle #2 here.
The following extension method is used with both solutions to deserialize the JSON string to a specified type because, confusingly, BsonExtensionMethods
has a ToJson()
method but no FromJson()
method:
public static partial class MyBsonExtensionMethods
{
// Not sure why but BsonExtensionMethods.cs seems to lack methods for deserializing from JSON, so I added some here.
// See https://github.com/mongodb/mongo-csharp-driver/blob/master/src/MongoDB.Bson/BsonExtensionMethods.cs
public static TNominalType FromJson<TNominalType>(
string json,
JsonReaderSettings readerSettings = null,
IBsonSerializer<TNominalType> serializer = null,
Action<BsonDeserializationContext.Builder> configurator = null)
{
return (TNominalType)FromJson(json, typeof(TNominalType), readerSettings, serializer, configurator);
}
public static object FromJson(
string json,
Type nominalType,
JsonReaderSettings readerSettings = null,
IBsonSerializer serializer = null,
Action<BsonDeserializationContext.Builder> configurator = null)
{
if (nominalType == null || json == null)
throw new ArgumentNullException();
serializer = serializer ?? BsonSerializer.LookupSerializer(nominalType);
if (serializer.ValueType != nominalType)
throw new ArgumentException(string.Format("serializer.ValueType {0} != nominalType {1}.", serializer.GetType().FullName, nominalType.FullName), "serializer");
using (var textReader = new StringReader(json))
using (var reader = new JsonReader(textReader, readerSettings ?? JsonReaderSettings.Defaults))
{
var context = BsonDeserializationContext.CreateRoot(reader, configurator);
return serializer.Deserialize(context);
}
}
}
Upvotes: 3