I have the following class I am unsuccessfully attempting to serialize to Json.
class HL7 : NameValueCollection
{
public List<HL7> Children { get; set; }
public HL7()
{
Children = new List<HL7>();
}
}
I have created the object like so and added data to it:
HL7 hl7 = new HL7();
hl7.Add("a", "123");
hl7.Add("b", "456");
hl7.Children.Add(new HL7());
hl7.Children[0].Add("c", "123");
hl7.Children[0].Add("d", "456");
When I call
JsonConvert.SerializeObject(hl7)
I receive
["a","b"]
I was expecting the following:
{
"a": "123",
"b": "456",
"Children": [
{
"c": "123",
"d": "456",
}
]
}
There are a few things going on here:
Json.NET cannot serialize a NameValueCollection without a custom converter because NameValueCollection implements IEnumerable for iterating over the keys, but does not implement IDictionary for iterating over keys and values. See this answer for a fuller explanation of why this causes problems for Json.NET.
Because NameValueCollection implements IEnumerable, Json.NET sees your class as a collection, and so serializes it as a JSON array and not a JSON object with named properties. Thus, your Children are not serialized. Again, a custom converter would be required to fix this.
Assuming the above issues are resolved, if your HL7 subclass of NameValueCollection happens to have a key named "Children" you will generate invalid JSON when serializing it, namely an object with duplicated property names. I suggest moving the names & values into a nested property (named, e.g., "Values") for purposes of unambiguous serialization.
NameValueCollection actually can have multiple string values for a given key string, so its entry values need to be serialized as a JSON array not a single string.
Putting all this together, the following code:
[JsonConverter(typeof(HL7Converter))]
public class HL7 : NameValueCollection
{
public List<HL7> Children { get; set; }
public HL7()
{
Children = new List<HL7>();
}
}
public class HL7Converter : JsonConverter
{
class HL7Proxy
{
public NameValueCollectionDictionaryWrapper Values { get; set; }
public List<HL7> Children { get; set; }
}
public override bool CanConvert(Type objectType)
{
return objectType == typeof(HL7);
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
var proxy = serializer.Deserialize<HL7Proxy>(reader);
if (proxy == null)
return existingValue;
var hl7 = existingValue as HL7;
if (hl7 == null)
hl7 = new HL7();
hl7.Add(proxy.Values.GetCollection());
if (proxy.Children != null)
hl7.Children.AddRange(proxy.Children);
return hl7;
}
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
HL7 hl7 = (HL7)value;
if (hl7 == null)
return;
serializer.Serialize(writer, new HL7Proxy { Children = hl7.Children, Values = new NameValueCollectionDictionaryWrapper(hl7) });
}
}
// Proxy dictionary to serialize & deserialize a NameValueCollection. We use a proxy dictionary rather than a real dictionary because NameValueCollection is an ordered collection but the generic dictionary class is unordered.
public class NameValueCollectionDictionaryWrapper: IDictionary<string, string []>
{
readonly NameValueCollection collection;
public NameValueCollectionDictionaryWrapper()
: this(new NameValueCollection())
{
}
public NameValueCollectionDictionaryWrapper(NameValueCollection collection)
{
this.collection = collection;
}
// Method instead of a property to guarantee that nobody tries to serialize it.
public NameValueCollection GetCollection()
{
return collection;
}
#region IDictionary<string,string[]> Members
public void Add(string key, string[] value)
{
if (collection.GetValues(key) != null)
throw new ArgumentException("Duplicate key " + key);
foreach (var str in value)
collection.Add(key, str);
}
public bool ContainsKey(string key)
{
return collection.GetValues(key) != null;
}
public ICollection<string> Keys
{
get {
return collection.AllKeys;
}
}
public bool Remove(string key)
{
bool found = ContainsKey(key);
if (found)
collection.Remove(key);
return found;
}
public bool TryGetValue(string key, out string[] value)
{
value = collection.GetValues(key);
return value != null;
}
public ICollection<string[]> Values
{
get {
return Enumerable.Range(0, collection.Count).Select(i => collection.GetValues(i)).ToArray();
}
}
public string[] this[string key]
{
get
{
var value = collection.GetValues(key);
if (value == null)
throw new KeyNotFoundException();
return value;
}
set
{
Remove(key);
Add(key, value);
}
}
#endregion
#region ICollection<KeyValuePair<string,string[]>> Members
public void Add(KeyValuePair<string, string[]> item)
{
Add(item.Key, item.Value);
}
public void Clear()
{
collection.Clear();
}
public bool Contains(KeyValuePair<string, string[]> item)
{
string [] value;
if (!TryGetValue(item.Key, out value))
return false;
return EqualityComparer<string[]>.Default.Equals(item.Value, value); // Consistent with Dictionary<TKey, TValue>
}
public void CopyTo(KeyValuePair<string, string[]>[] array, int arrayIndex)
{
foreach (var item in this)
array[arrayIndex++] = item;
}
public int Count
{
get { return collection.Count; }
}
public bool IsReadOnly
{
get { return false; }
}
public bool Remove(KeyValuePair<string, string[]> item)
{
if (Contains(item))
return Remove(item.Key);
return false;
}
#endregion
#region IEnumerable<KeyValuePair<string,string[]>> Members
public IEnumerator<KeyValuePair<string, string[]>> GetEnumerator()
{
foreach (string key in collection)
{
yield return new KeyValuePair<string, string[]>(key, collection.GetValues(key));
}
}
#endregion
#region IEnumerable Members
System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
#endregion
}
Using the following test case:
HL7 hl7 = new HL7();
hl7.Add("a", "123");
hl7.Add("b", "456");
hl7.Add("Children", "Children");
hl7.Children.Add(new HL7());
hl7.Children[0].Add("c", "123");
hl7.Children[0].Add("d", "456");
hl7.Children[0].Add("d", "789");
var json = JsonConvert.SerializeObject(hl7, Formatting.Indented);
Debug.WriteLine(json);
Gives the following JSON:
{
"Values": {
"a": [
"123"
],
"b": [
"456"
],
"Children": [
"Children"
]
},
"Children": [
{
"Values": {
"c": [
"123"
],
"d": [
"456",
"789"
]
},
"Children": []
}
]
}
Inspired by this answer
how to convert NameValueCollection to JSON string?
, here is the working code (the only bad part is probably the "Children" string that is the property name. If you'll do a refactor, this will cause an error.
JsonConvert.SerializeObject(NvcToDictionary(hl7, false));
And the function:
static Dictionary<string, object> NvcToDictionary(HL7 nvc, bool handleMultipleValuesPerKey)
{
var result = new Dictionary<string, object>();
foreach (string key in nvc.Keys)
{
if (handleMultipleValuesPerKey)
{
string[] values = nvc.GetValues(key);
if (values.Length == 1)
{
result.Add(key, values[0]);
}
else
{
result.Add(key, values);
}
}
else
{
result.Add(key, nvc[key]);
}
}
if (nvc.Children.Any())
{
var listOfChildrenDictionary = new List<Dictionary<string, object>>();
foreach (var nvcChildren in nvc.Children){
listOfChildrenDictionary.Add(NvcToDictionary(nvcChildren, false));
}
result.Add("Children", listOfChildrenDictionary);
}
return result;
}
I have had issues with serializing NameValueCollections, using JSON.Net, The only way I have found is to convert it to a dictionary and then serialize it like:
var jsonString = JsonConvert.SerializeObject(new
{
Parent = hl7.AllKeys.ToDictionary(r => r, r => hl7[r]),
Children = hl7.Children.Select(c => c.AllKeys.ToDictionary(sub => sub, sub => c[sub]))
}, Newtonsoft.Json.Formatting.Indented);
and you will end up with:
{
"Parent": {
"a": "123",
"b": "456"
},
"Children": [
{
"c": "123",
"d": "456"
}
]
}
But this will return "Parent" as well for top level items, since you have to specify a name for property in anonymous type
Here's a custom serializer that will write the JSON as you were looking for, example program attached. The serializer is at the bottom. Note that you will need to add this converter to the JSON serializer settings, either through the default as I've done, or through the constructor of your serializer. Alternately, since you have a subclass you can use the JsonConverterAttribute on the HL7 class to assign the serializer
public class Program
{
static int Main(string[] args) {
JsonConvert.DefaultSettings = () => new JsonSerializerSettings {
Converters = new []{ new HL7Converter() }
};
HL7 hl7 = new HL7();
hl7.Add("a", "123");
hl7.Add("b", "456");
hl7.Children.Add(new HL7());
hl7.Children[0].Add("c", "123");
hl7.Children[0].Add("d", "456");
Console.WriteLine (JsonConvert.SerializeObject (hl7));
return 0;
}
}
public class HL7 : NameValueCollection
{
public List<HL7> Children { get; set; }
public HL7()
{
Children = new List<HL7> ();
}
}
public class HL7Converter : Newtonsoft.Json.JsonConverter {
#region implemented abstract members of JsonConverter
public override void WriteJson (Newtonsoft.Json.JsonWriter writer, object value, Newtonsoft.Json.JsonSerializer serializer)
{
var collection = (HL7)value;
writer.WriteStartObject ();
foreach (var key in collection.AllKeys) {
writer.WritePropertyName (key);
writer.WriteValue (collection [key]);
}
writer.WritePropertyName ("Children");
serializer.Serialize (writer,collection.Children);
writer.WriteEndObject ();
}
public override object ReadJson (Newtonsoft.Json.JsonReader reader, Type objectType, object existingValue, Newtonsoft.Json.JsonSerializer serializer)
{
HL7 collection = existingValue as HL7 ?? new HL7 ();
JObject jObj = JObject.Load (reader);
foreach (var prop in jObj.Properties()) {
if (prop.Name != "Children") {
collection.Add (prop.Name, prop.Value.ToObject<string> ());
} else {
collection.Children = jObj.ToObject<List<HL7>> ();
}
}
return collection;
}
public override bool CanConvert (Type objectType)
{
return objectType == typeof(HL7);
}
#endregion
}
Related
We have an interface and a base class with multiple derived types.
public interface IEvent
{
[JsonProperty("id")]
public string Id { get; set; }
string Type { get; }
}
public abstract class EventBase: IEvent
{
public string Id { get; set; }
public abstract string Type { get; }
}
public class UserCreated : EventBase
{
public override string Type { get; } = typeof(UserCreated).AssemblyQualifiedName;
}
public class UserUpdated : EventBase
{
public override string Type { get; } = typeof(UserUpdated).AssemblyQualifiedName;
}
We are storing these events of different derived types in the same container in Cosmos DB using v3 of .Net SDK Microsoft.Azure.Cosmos. We then want to read all the events and have them deserialized to the correct type.
public class CosmosDbTests
{
[Fact]
public async Task TestFetchingDerivedTypes()
{
var endpoint = "";
var authKey = "";
var databaseId ="";
var containerId="";
var client = new CosmosClient(endpoint, authKey);
var container = client.GetContainer(databaseId, containerId);
await container.CreateItemAsync(new UserCreated{ Id = Guid.NewGuid().ToString() });
await container.CreateItemAsync(new UserUpdated{ Id = Guid.NewGuid().ToString() });
var queryable = container.GetItemLinqQueryable<IEvent>();
var query = queryable.ToFeedIterator();
var list = new List<IEvent>();
while (query.HasMoreResults)
{
list.AddRange(await query.ReadNextAsync());
}
Assert.NotEmpty(list);
}
}
Doesn't seem to be any option to tell GetItemLinqQueryable how to handle types. Is there any other method or approach to support multiple derived types in one query?
It's ok to put the events in some kind of wrapper entity if that would help, but they aren't allowed to be stored as an serialized sting inside a property.
The comment from Stephen Clearly pointed me in the right direction and with the help of this blog https://thomaslevesque.com/2019/10/15/handling-type-hierarchies-in-cosmos-db-part-2/ I ended up with a solution similar to the following example were we have a custom CosmosSerializer that uses a custom JsonConverter that reads the Type property.
public interface IEvent
{
[JsonProperty("id")]
public string Id { get; set; }
[JsonProperty("$type")]
string Type { get; }
}
public abstract class EventBase: IEvent
{
public string Id { get; set; }
public string Type => GetType().AssemblyQualifiedName;
}
public class UserCreated : EventBase
{
}
public class UserUpdated : EventBase
{
}
EventJsonConverter reads the Type property.
public class EventJsonConverter : JsonConverter
{
// This converter handles only deserialization, not serialization.
public override bool CanRead => true;
public override bool CanWrite => false;
public override bool CanConvert(Type objectType)
{
// Only if the target type is the abstract base class
return objectType == typeof(IEvent);
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
// First, just read the JSON as a JObject
var obj = JObject.Load(reader);
// Then look at the $type property:
var typeName = obj["$type"]?.Value<string>();
return typeName == null ? null : obj.ToObject(Type.GetType(typeName), serializer);
}
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
throw new NotSupportedException("This converter handles only deserialization, not serialization.");
}
}
The NewtonsoftJsonCosmosSerializer takes a JsonSerializerSettings that it uses for serialization.
public class NewtonsoftJsonCosmosSerializer : CosmosSerializer
{
private static readonly Encoding DefaultEncoding = new UTF8Encoding(false, true);
private readonly JsonSerializer _serializer;
public NewtonsoftJsonCosmosSerializer(JsonSerializerSettings settings)
{
_serializer = JsonSerializer.Create(settings);
}
public override T FromStream<T>(Stream stream)
{
if (typeof(Stream).IsAssignableFrom(typeof(T)))
{
return (T)(object)stream;
}
using var sr = new StreamReader(stream);
using var jsonTextReader = new JsonTextReader(sr);
return _serializer.Deserialize<T>(jsonTextReader);
}
public override Stream ToStream<T>(T input)
{
var streamPayload = new MemoryStream();
using var streamWriter = new StreamWriter(streamPayload, encoding: DefaultEncoding, bufferSize: 1024, leaveOpen: true);
using JsonWriter writer = new JsonTextWriter(streamWriter);
writer.Formatting = _serializer.Formatting;
_serializer.Serialize(writer, input);
writer.Flush();
streamWriter.Flush();
streamPayload.Position = 0;
return streamPayload;
}
}
The CosmosClient is now created with our own NewtonsoftJsonCosmosSerializer and EventJsonConverter.
public class CosmosDbTests
{
[Fact]
public async Task TestFetchingDerivedTypes()
{
var endpoint = "";
var authKey = "";
var databaseId ="";
var containerId="";
var client = new CosmosClient(endpoint, authKey, new CosmosClientOptions
{
Serializer = new NewtonsoftJsonCosmosSerializer(new JsonSerializerSettings
{
Converters = { new EventJsonConverter() }
})
});
var container = client.GetContainer(databaseId, containerId);
await container.CreateItemAsync(new UserCreated{ Id = Guid.NewGuid().ToString() });
await container.CreateItemAsync(new UserUpdated{ Id = Guid.NewGuid().ToString() });
var queryable = container.GetItemLinqQueryable<IEvent>();
var query = queryable.ToFeedIterator();
var list = new List<IEvent>();
while (query.HasMoreResults)
{
list.AddRange(await query.ReadNextAsync());
}
Assert.NotEmpty(list);
}
}
Hopefully someone can help me with my issue.
I'm working with a 3rd party API that, depending on the configuration of the product in the backend, will return a different structure of JSON as the response.
I've included JSON samples showing the different outcomes depending on the product. There are 3 scenarios for the FreeGifts section of the product:
Has 2 or more free gifts.
Has 1 free gifts.
Has no free gifts
Scenario 1
{
"FreeGifts": [{
"FreeGift": [{
"SKU": "BOWS-SMALL-ALFIE"
},
{
"SKU": "BOWS-LARGE-ALONZO"
},
{
"SKU": "BOWS-LARGE-CLANCY"
},
{
"SKU": "BOWS-SMALL-ALVIN"
},
{
"SKU": "BOWS-SMALL-CLARK"
}
]
}]
}
Scenario 2
{
"FreeGifts": [{
"FreeGift": {
"SKU": "BOWS-SMALL-ALVIN"
}
}]
}
Scenario 3
{
"FreeGifts": [
""
]
}
Sites like http://json2csharp.com/ and https://jsonutils.com/ provide me with 3 different class definitions depending on the scenario.
If I had only 1 of these in the structure I could probably deal with it but I have around 7 or 8. It's impossible for me to cater for it.
I'm completely stumped as to how I get Newtonsoft.json to work with the ambiguity that the API produces.
Do I need to go back to the provider and ask them if they can change it?!?
You can use SingleOrArrayConverter<FreeGift> from this answer to How to handle both a single item and an array for the same property using JSON.net by Brian Rogers along with TolerantObjectCollectionConverter<FreeGifts> from this answer to How can I ignore a blank array inside an array of JSON objects while deserializing? to successfully deserialize all 3 JSON variants. To do so, define your model and apply the appropriate JSON converter as follows:
public class Root
{
[JsonConverter(typeof(TolerantObjectCollectionConverter<FreeGifts>))]
public List<FreeGifts> FreeGifts { get; set; }
}
public class FreeGifts
{
[JsonConverter(typeof(SingleOrArrayConverter<FreeGift>))]
public List<FreeGift> FreeGift { get; set; }
}
public class FreeGift
{
public string SKU { get; set; }
}
class SingleOrArrayConverter<T> : JsonConverter
{
// Taken from the answer to
// https://stackoverflow.com/questions/18994685/how-to-handle-both-a-single-item-and-an-array-for-the-same-property-using-json-n
// https://stackoverflow.com/a/18997172
// by Brian Rogers
public override bool CanConvert(Type objectType)
{
return (objectType == typeof(List<T>));
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
var tokenType = reader.SkipComments().TokenType;
if (tokenType == JsonToken.Null)
return null;
var list = existingValue as List<T> ?? new List<T>();
if (tokenType == JsonToken.StartArray)
{
serializer.Populate(reader, list);
}
else
{
list.Add(serializer.Deserialize<T>(reader));
}
return list;
}
public override bool CanWrite { get { return false; } }
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
throw new NotImplementedException();
}
}
public class TolerantObjectCollectionConverter<TItem> : JsonConverter
{
// Taken from the answer to
// https://stackoverflow.com/questions/49030516/how-can-i-ignore-a-blank-array-inside-an-array-of-json-objects-while-deserializi
// https://stackoverflow.com/a/49078620/
public override bool CanConvert(Type objectType)
{
return !objectType.IsArray && objectType != typeof(string) && typeof(ICollection<TItem>).IsAssignableFrom(objectType);
}
public override bool CanWrite { get { return false; } }
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
throw new NotImplementedException();
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
// Get contract information
var contract = serializer.ContractResolver.ResolveContract(objectType) as JsonArrayContract;
if (contract == null || contract.IsMultidimensionalArray || objectType.IsArray)
throw new JsonSerializationException(string.Format("Invalid array contract for {0}", objectType));
// Process the first token
var tokenType = reader.SkipComments().TokenType;
if (tokenType == JsonToken.Null)
return null;
if (tokenType != JsonToken.StartArray)
throw new JsonSerializationException(string.Format("Expected {0}, encountered {1} at path {2}", JsonToken.StartArray, reader.TokenType, reader.Path));
// Allocate the collection
var collection = existingValue as ICollection<TItem> ?? (ICollection<TItem>)contract.DefaultCreator();
// Process the collection items
while (reader.Read())
{
switch (reader.TokenType)
{
case JsonToken.EndArray:
return collection;
case JsonToken.StartObject:
case JsonToken.Null:
collection.Add(serializer.Deserialize<TItem>(reader));
break;
default:
reader.Skip();
break;
}
}
// Should not come here.
throw new JsonSerializationException("Unclosed array at path: " + reader.Path);
}
}
public static partial class JsonExtensions
{
public static JsonReader SkipComments(this JsonReader reader)
{
while (reader.TokenType == JsonToken.Comment && reader.Read())
;
return reader;
}
}
Notes:
[JsonConverter(typeof(TolerantObjectCollectionConverter<FreeGifts>))] handles the fact that the upper-level "FreeGifts": [] array may sometimes contain an unwanted string value. The value is simply skipped.
[JsonConverter(typeof(SingleOrArrayConverter<FreeGift>))] handles the fact that the "FreeGift" property value may sometimes be either a single object or an array of objects.
You could combine the two converters in situations where a property value might be an array with invalid items or a single object not contained in an array. However, this isn't the case in the three JSON examples shown.
Sample working .Net fiddle.
Is there any way to convert any of my custom attribute(s) into something when I call the JsonConvert.SerializeObject(...) function? For example, I have a class:
class A
{
[UnitAttribute("---")]
public double? Ratio { get; set; }
}
When serialize any instance of such class, is there any way to put the value of the UnitAttribute into the Json string?
I found there is a IAttributeProvider interface in the API. But it seems the serialize function doesn't really use it.
The simplest thing to do would be to create a JsonConverter that adds the attribute text (which I assume corresponds to units, in this case) and attach the converter to the property:
class A
{
[JsonConverter(typeof(UnitConverter), new object [] { "mm" })]
public double? Ratio { get; set; }
}
public class UnitConverter : JsonConverter
{
public string Units { get; set; }
string UnitsPostfix { get { return string.IsNullOrEmpty(Units) ? string.Empty : " " + Units; } }
public UnitConverter(string units)
{
this.Units = units;
}
public override bool CanConvert(Type objectType)
{
throw new NotImplementedException(); // Not called when applied directly to a property.
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
var jvalue = JValue.Load(reader);
if (jvalue.Type == JTokenType.String)
{
var s = (string)jvalue;
if (s.EndsWith(Units))
jvalue = (JValue)s.Substring(0, s.LastIndexOf(Units)).Trim();
}
return jvalue.ToObject(objectType);
}
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
var jvalue = JValue.FromObject(value);
if (jvalue.Type == JTokenType.Null)
jvalue.WriteTo(writer);
else
writer.WriteValue((string)jvalue + UnitsPostfix);
}
}
Notice I can pass the unit string directly to the converter's constructor in the attribute declaration.
If you have lots of fields and properties in your code base with UnitAttribute and want to apply the converter to all of them automatically, you could create a custom IContractResolver derived from an existing contract resolver such as DefaultContractResolver that applies the necessary converter:
public class UnitContractResolver : DefaultContractResolver
{
protected override JsonProperty CreateProperty(MemberInfo member, MemberSerialization memberSerialization)
{
var property = base.CreateProperty(member, memberSerialization);
if (property.Converter == null && property.MemberConverter == null)
{
var attr = property.AttributeProvider.GetAttributes(typeof(UnitAttribute), true).Cast<UnitAttribute>().Where(a => !string.IsNullOrEmpty(a.Name)).FirstOrDefault();
if (attr != null)
{
property.Converter = property.MemberConverter = new UnitConverter(attr.Name);
}
}
return property;
}
}
You can either use the the contract resolver explicitly, like so:
var settings = new JsonSerializerSettings() { ContractResolver = new UnitContractResolver() };
var json = JsonConvert.SerializeObject(a, settings);
Debug.WriteLine(json);
var a11 = JsonConvert.DeserializeObject<A>(json, settings);
Debug.Assert(a.Ratio == a.Ratio);
Or set it in the Json.net global settings for automatic use.
Is it possible to skip a part of json when contract type does not match. Namely, I receive a json response which does not conform the schema - null objects are serialized as empty arrays ("thank's" to php serializer). So, instead of "null" or "{}" I get "[]".
So far, I tried to use Error event handler available in JsonSerializerSettings. However, this approach is rather ugly because Json.net does not return meaningful exception type.
Example of valid response:
{
"key": "key",
"id": "id",
"status": "status",
"opts": {
"start": 42,
"limit": 12
}
}
}
Example of invalid response
{
"key": "key",
"id": "id",
"status": "status",
"opts": []
}
You can handle this issue using a custom JsonConverter. The converter can detect the format of the opts property in the JSON and then either deserialize it or set its value to null in the target object. Here's how I would write the converter:
class OptsConverter : JsonConverter
{
public override bool CanConvert(Type objectType)
{
return (objectType == typeof(Opts));
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
JToken token = JToken.Load(reader);
if (token.Type == JTokenType.Object)
{
return new Opts
{
start = (int)token["start"],
limit = (int)token["limit"]
};
}
return null;
}
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
throw new NotImplementedException();
}
}
Here is a demo:
class Program
{
static void Main(string[] args)
{
string valid = #"
{
""key"": ""key"",
""id"": ""id"",
""status"": ""status"",
""opts"": {
""start"": 42,
""limit"": 12
}
}";
DeserializeAndWriteToConsole(valid);
string invalid = #"
{
""key"": ""key"",
""id"": ""id"",
""status"": ""status"",
""opts"": []
}";
DeserializeAndWriteToConsole(invalid);
}
private static void DeserializeAndWriteToConsole(string json)
{
RootObject root = JsonConvert.DeserializeObject<RootObject>(json, new OptsConverter());
Console.WriteLine("key: " + root.key);
Console.WriteLine("id: " + root.id);
Console.WriteLine("status: " + root.status);
if (root.opts != null)
{
Console.WriteLine("opts.start: " + root.opts.start);
Console.WriteLine("opts.limit: " + root.opts.limit);
}
else
{
Console.WriteLine("opts: (null)");
}
Console.WriteLine();
}
}
public class RootObject
{
public string key { get; set; }
public string id { get; set; }
public string status { get; set; }
public Opts opts { get; set; }
}
public class Opts
{
public int start { get; set; }
public int limit { get; set; }
}
Output:
key: key
id: id
status: status
opts.start: 42
opts.limit: 12
key: key
id: id
status: status
opts: (null)
I'm inserting raw JSON into a collection and finding that what is stored in the database is missing the values. For example, my collection is a collection of BsonDocuments:
_products = database.GetCollection<BsonDocument>("products");
The code to insert the JSON into the collection:
public int AddProductDetails(JObject json)
{
var doc = json.ToBsonDocument(DictionarySerializationOptions.Document);
_products.Insert(doc);
}
The JSON that is passed in looks like this:
{
"Id": 1,
"Tags": [
"book",
"database"
],
"Name": "Book Name",
"Price": 12.12
}
But, what is persisted in the collection is just the properties with no values.
{
"_id": {
"$oid": "5165c7e10fdb8c09f446d720"
},
"Id": [],
"Tags": [
[],
[]
],
"Name": [],
"Price": []
}
Why are the values being dropped?
This does what I was expecting.
public int AddProductDetails(JObject json)
{
BsonDocument doc = BsonDocument.Parse(json.ToString());
_products.Insert(doc);
}
I ran into this issue when I had a C# class with a property of type JObject.
My Solution was to create JObjectSerializer for MondoDB and add the attribute to the property so Mongo serializer uses it. I assume if I tried hard enough I could register the below serializer in Mongo as the global one for this type as well.
Register serializer for property processing:
[BsonSerializer(typeof(JObjectSerializer))]
public JObject AdditionalData { get; set; }
The serializer itself:
public class JObjectSerializer : SerializerBase<JObject> // IBsonSerializer<JObject>
{
public override JObject Deserialize(BsonDeserializationContext context, BsonDeserializationArgs args)
{
var myBSONDoc = BsonDocumentSerializer.Instance.Deserialize(context);
return JObject.Parse(myBSONDoc.ToString());
}
public override void Serialize(BsonSerializationContext context, BsonSerializationArgs args, JObject value)
{
var myBSONDoc = MongoDB.Bson.BsonDocument.Parse(value.ToString());
BsonDocumentSerializer.Instance.Serialize(context, myBSONDoc);
}
}
The problem when using JObject.ToString, BsonDocument.Parse, etc. is the performance is not very good because you do the same operations multiple times, you do string allocations, parsing, etc.
So, I have written a function that converts a JObject to an IEnumerable<KeyValuePair<string, object>> (only using enumerations), which is a type usable by one of the BsonDocument constructors. Here is the code:
public static BsonDocument ToBsonDocument(this JObject jo)
{
if (jo == null)
return null;
return new BsonDocument(ToEnumerableWithObjects(jo));
}
public static IEnumerable<KeyValuePair<string, object>> ToEnumerableWithObjects(this JObject jo)
{
if (jo == null)
return Enumerable.Empty<KeyValuePair<string, object>>();
return new JObjectWrapper(jo);
}
private class JObjectWrapper : IEnumerable<KeyValuePair<string, object>>
{
private JObject _jo;
public JObjectWrapper(JObject jo)
{
_jo = jo;
}
public IEnumerator<KeyValuePair<string, object>> GetEnumerator() => new JObjectWrapperEnumerator(_jo);
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
public static object ToValue(JToken token)
{
object value;
switch (token.Type)
{
case JTokenType.Object:
value = new JObjectWrapper((JObject)token);
break;
case JTokenType.Array:
value = new JArrayWrapper((JArray)token);
break;
default:
if (token is JValue jv)
{
value = ((JValue)token).Value;
}
else
{
value = token.ToString();
}
break;
}
return value;
}
}
private class JArrayWrapper : IEnumerable
{
private JArray _ja;
public JArrayWrapper(JArray ja)
{
_ja = ja;
}
public IEnumerator GetEnumerator() => new JArrayWrapperEnumerator(_ja);
}
private class JArrayWrapperEnumerator : IEnumerator
{
private IEnumerator<JToken> _enum;
public JArrayWrapperEnumerator(JArray ja)
{
_enum = ja.GetEnumerator();
}
public object Current => JObjectWrapper.ToValue(_enum.Current);
public bool MoveNext() => _enum.MoveNext();
public void Reset() => _enum.Reset();
}
private class JObjectWrapperEnumerator : IEnumerator<KeyValuePair<string, object>>
{
private IEnumerator<KeyValuePair<string, JToken>> _enum;
public JObjectWrapperEnumerator(JObject jo)
{
_enum = jo.GetEnumerator();
}
public KeyValuePair<string, object> Current => new KeyValuePair<string, object>(_enum.Current.Key, JObjectWrapper.ToValue(_enum.Current.Value));
public bool MoveNext() => _enum.MoveNext();
public void Dispose() => _enum.Dispose();
public void Reset() => _enum.Reset();
object IEnumerator.Current => Current;
}
Have you tried using the BsonSerializer?
using MongoDB.Bson.Serialization;
[...]
var document = BsonSerializer.Deserialize<BsonDocument>(json);
BsonSerializer works with strings, so if the JSON argument is a JObject(or JArray, JRaw etc) you have to serialize it with JsonConvert.SerializeObject()
Here is an updated version of Andrew DeVries's answer that includes handling for serializing/deserializing null values.
public class JObjectSerializer : SerializerBase<JObject>
{
public override JObject Deserialize(BsonDeserializationContext context, BsonDeserializationArgs args)
{
if (context.Reader.CurrentBsonType != BsonType.Null)
{
var myBSONDoc = BsonDocumentSerializer.Instance.Deserialize(context);
return JObject.Parse(myBSONDoc.ToStrictJson());
}
else
{
context.Reader.ReadNull();
return null;
}
}
public override void Serialize(BsonSerializationContext context, BsonSerializationArgs args, JObject value)
{
if (value != null)
{
var myBSONDoc = BsonDocument.Parse(value.ToString());
BsonDocumentSerializer.Instance.Serialize(context, myBSONDoc);
}
else
{
context.Writer.WriteNull();
}
}
}
The ToStrictJson() call is an extension method that wraps the call to the built-in BSON ToJson() method to include setting the output mode to strict. If this is not done, the parsing will fail because BSON type constructors will remain in the JSON output (ObjectId(), for example).
Here is the implementation of ToStrictJson() as well:
public static class MongoExtensionMethods
{
/// <summary>
/// Create a JsonWriterSettings object to use when serializing BSON docs to JSON.
/// This will force the serializer to create valid ("strict") JSON.
/// Without this, Object IDs and Dates are ouput as {"_id": ObjectId(ds8f7s9d87f89sd9f8d9f7sd9f9s8d)}
/// and {"date": ISODate("2020-04-14 14:30:00:000")} respectively, which is not valid JSON
/// </summary>
private static JsonWriterSettings jsonWriterSettings = new JsonWriterSettings()
{
OutputMode = JsonOutputMode.Strict
};
/// <summary>
/// Custom extension method to convert MongoDB objects to JSON using the OutputMode = Strict setting.
/// This ensure that the resulting string is valid JSON.
/// </summary>
/// <typeparam name="TNominalType">The type of object to convert to JSON</typeparam>
/// <param name="obj">The object to conver to JSON</param>
/// <returns>A strict JSON string representation of obj.</returns>
public static string ToStrictJson<TNominalType>(this TNominalType obj)
{
return BsonExtensionMethods.ToJson<TNominalType>(obj, jsonWriterSettings);
}
}
I use the following. It's based on Simon's answer, thanks for the idea, and works in the same way, avoiding unnecessary serialization / deserialization into string.
It's just a bit more compact, thanks to Linq and C# 10:
public static BsonDocument ToBsonDocument(this JObject o) =>
new(o.Properties().Select(p => new BsonElement(p.Name, p.Value.ToBsonValue())));
public static BsonValue ToBsonValue(this JToken t) =>
t switch
{
JObject o => o.ToBsonDocument(),
JArray a => new BsonArray(a.Select(ToBsonValue)),
JValue v => BsonValue.Create(v.Value),
_ => throw new NotSupportedException($"ToBsonValue: {t}")
};
Most of the answers here involve serializing to and then deserializing from a string. Here is a solution that serializes to/from raw BSON instead. It requires the Newtonsoft.Json.Bson nuget package.
using System.IO;
using MongoDB.Bson.Serialization;
using MongoDB.Bson.Serialization.Serializers;
using Newtonsoft.Json;
using Newtonsoft.Json.Bson;
using Newtonsoft.Json.Linq;
namespace Zonal.EventPublisher.Worker
{
public class JObjectSerializer : SerializerBase<JObject>
{
public override JObject Deserialize(BsonDeserializationContext context, BsonDeserializationArgs args)
{
using (var stream = new MongoDB.Bson.IO.ByteBufferStream(context.Reader.ReadRawBsonDocument()))
using (JsonReader reader = new BsonDataReader(stream))
{
return JObject.Load(reader);
}
}
public override void Serialize(BsonSerializationContext context, BsonSerializationArgs args, JObject value)
{
using (var stream = new MemoryStream())
using (JsonWriter writer = new BsonDataWriter(stream))
{
value.WriteTo(writer);
var buffer = new MongoDB.Bson.IO.ByteArrayBuffer(stream.ToArray());
context.Writer.WriteRawBsonDocument(buffer);
}
}
}
}
Don't forget to register the serializer with:
BsonSerializer.RegisterSerializer(new JObjectSerializer());
After that you can convert your JObject to a BsonDocument by using the MongoDB.Bson.BsonExtensionMethods.ToBsonDocument extension method:
var myBsonDocument = myJObject.ToBsonDocument()
And convert a BsonDocument back to a JObject by using the MongoDB.Bson.Serialization.BsonSerializer class:
var myJObject = BsonSerializer.Deserialize<JObject>(myBsonDocument);