Newtonsoft JSon Deserialize into Primitive type - c#

In my C# program, I am querying a webservice and getting a reply stream back in JSON that looks something like this:
{"val":12345.12},{"val":23456.23},{"val":34567.01},...
or, with possibly more than 1 value per reply object:
{"val1":12345.12,"val2":1},{"val1":23456.23,"val2":3},....
And I have the following code utilizing the Newtonsoft.Json library that parses the stream and performs some action on each parsed object, one at a time:
public void ParseReply<T>(StreamReader sr, Action<T> action)
{
using (var reader = new JsonTextReader(sr))
{
var ser = new JsonSerializer();
while (reader.Read())
{
if (reader.TokenType == JsonToken.EndArray)
break;
action(ser.Deserialize<T>(reader));
}
}
}
So, in the case of the second result, I have the following code:
public class MyObject
{
public double val1;
public double val2;
}
and then:
myJson.ParseReply<MyObject>(sr, obj => ...);
works perfectly.
But, in the case of the first reply (1 value per object), if I try and use my method in the following way:
myJson.ParseReply<double>(sr, dbl => ...);
I get an error saying:
Cannot deserialize the current JSON object (e.g. {"name":"value"}) into type 'System.Double' because the type requires a JSON primitive value (e.g. string, number, boolean, null) to deserialize correctly.
To fix this error either change the JSON to a JSON primitive value (e.g. string, number, boolean, null) 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<T>) 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.
I'm really lost / curious how I could update my code to be able to parse both of these correctly and I'm a bit lost on this error message. Any help would REALLY be appreciated!!

If you change your static method to return an IEnumerable<T> rather than taking an Action<T>, you will be able to chain together Enumerable and LINQ to JSON methods in a very concise and natural way. Thus if you have:
public static class JsonExtensions
{
public static IEnumerable<T> ParseArray<T>(this TextReader textReader)
{
using (var reader = new JsonTextReader(textReader))
{
bool inArray = false;
var ser = JsonSerializer.CreateDefault();
while (reader.Read())
{
if (reader.TokenType == JsonToken.Comment)
continue;
if (reader.TokenType == JsonToken.StartArray && !inArray)
{
inArray = true;
continue;
}
if (reader.TokenType == JsonToken.EndArray)
break;
yield return ser.Deserialize<T>(reader);
}
}
}
public static IEnumerable<JToken> DescendantsAndSelf(this JToken node)
{
// This method should be present on JToken but is only present on JContainer.
if (node == null)
return Enumerable.Empty<JToken>();
var container = node as JContainer;
if (container != null)
return container.DescendantsAndSelf();
else
return new[] { node };
}
public static bool IsNumeric(this JTokenType type) { return type == JTokenType.Integer || type == JTokenType.Float; }
public static bool IsNumeric(this JToken token) { return token == null ? false : token.Type.IsNumeric(); }
}
You can do:
var json = #"[{""val"":12345.12},{""val"":23456.23},{""val"":34567.01}]";
// Select all properties named "val" and transform their values to doubles.
foreach (var val in new StringReader(json).ParseArray<JToken>()
.Select(t => (double)t.SelectToken("val")))
{
Debug.WriteLine(val);
}
// Select all primitive numeric values
foreach (var val in new StringReader(json).ParseArray<JToken>()
.SelectMany(t => t.DescendantsAndSelf())
.Where(t => t.IsNumeric())
.Select(t => (double)t))
{
Debug.WriteLine(val);
}

Related

Converting a POCO object with JsonPropertyName decorations into a URL query string

Is there an .NET API to convert a POCO object with JsonPropertyName decorations into a properly encoded URL query string?
For example, for:
public record AuthEndPointArgs
{
[JsonPropertyName("response_type")]
public string? ResponseType { get; set; } // "code"
[JsonPropertyName("client_id")]
public string? ClientId { get; set; } // "a:b"
// ...
}
I expect: ?response_type=code&client_id=a%3Ab.
A home-grown version I'm using for now:
/// <summary>
/// Convert a shallow POCO object into query string,
/// with JsonPropertyName decorations becoming query string argument names
/// </summary>
public static string ConvertObjectToQueryString(object source, string query)
{
var uriArgs = System.Web.HttpUtility.ParseQueryString(query);
var jsonNode = System.Text.Json.JsonSerializer.SerializeToNode(source) ??
throw new InvalidOperationException(nameof(JsonSerializer.SerializeToNode));
foreach (var item in jsonNode.AsObject())
{
uriArgs[item.Key] = item.Value?.ToString() ?? String.Empty;
}
return uriArgs.ToString() ?? String.Empty;
}
Updated, for completeness, a reverse conversion:
/// <summary>
/// Convert a query string into a POCO object with string properties,
/// decorated with JsonPropertyName attributes
/// </summary>
public static T ConvertQueryStringToObject<T>(string query)
{
var args = System.Web.HttpUtility.ParseQueryString(query);
var jsonObject = new JsonObject();
foreach (var key in args.Keys.Cast<string>())
{
jsonObject.Add(key, JsonValue.Create(args[key]));
}
return jsonObject.Deserialize<T>() ??
throw new InvalidOperationException(typeof(T).Name);
}
To my knowledge there is no native .NET API, it seems like ASP.NET (Core) has support for reading it to some extent though (check here and here) but I can't tell how to create one.
The laziest solution would probably be to just serialize your object to JSON, and then HttpUtility.UrlEncode(json), then pass that to a query param, which would like so:
&payload=%7B%22response_type%22%3A%20%22code%22%2C%22client_id%22%3A%20%22a%3Ab%22%7D
At the other end just JsonSerializer.Deserialize<AuthEndPointArgs>(HttpUtility.UrlDecode(payload)) like so. This is assuming you can edit both ends.
While it sounds kinda stupid, it works, at in certain terms may even be better than serializing your AuthEndPointArgs to a query string directly, because the standard for a query string lacks some definitions, like how to deal with arrays, also complex options. It seems like the JS and PHP community have unofficial standards, but they require a manual implementation on both ends. So we'll also need to roll our own "standard" and implementation, unless we say that we can only serialize an object that fulfils the following criteria:
No complex objects as properties
No lists/ arrays as properties
Side note: URLs have a maximum length depending on a lot of factors, and by sending complex objects via query parameters you may go above that limit pretty fast, see here for more on this topic. It may just be best to hardcode something like ToQueryParams like Ady suggested in their answer
If we do want a generic implementation that aligns with those criteria, our implementation is actually quite simple:
public static class QueryStringSerializer
{
public static string Serialize(object source)
{
var props = source.GetType().GetProperties(
BindingFlags.Instance | BindingFlags.Public
);
var output = new StringBuilder();
foreach (var prop in props)
{
// You might want to extend this check, things like 'Guid'
// serialize nicely to a query string but aren't primitive types
if (prop.PropertyType.IsPrimitive || prop.PropertyType == typeof(string))
{
var value = prop.GetValue(source);
if (value is null)
continue;
output.Append($"{GetNameFromMember(prop)}={HttpUtility.UrlEncode(value.ToString())}");
}
else
throw new NotSupportedException();
}
}
}
private static string GetNameFromMember(MemberInfo prop)
{
string propName;
// You could also implement a 'QueryStringPropertyNameAttribute'
// if you want to be able to override the name given, for this you can basically copy the JSON attribute
// https://github.com/dotnet/runtime/blob/main/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Attributes/JsonPropertyNameAttribute.cs
if (Attribute.IsDefined(prop, typeof(JsonPropertyNameAttribute)))
{
var attribute = Attribute.GetCustomAttribute(prop, typeof(JsonPropertyNameAttribute)) as JsonPropertyNameAttribute;
// This check is technically unnecessary, but VS wouldn't shut up
if (attribute is null)
propName = prop.Name;
else
propName = attribute.Name;
}
else
propName = prop.Name;
return propName;
}
If we want to support objects with enumerables as properties or with "complex" objects as members we need to define how to serialize them, something like
class Foo
{
public int[] Numbers { get; set; }
}
Could be serialized to
?numbers[]=1&numbers[]=2
Or to a 1 indexed "list"
?numbers[1]=1&numbers[2]=2
Or to a comma delimited list
?numbers=1,2
Or just multiple of one instance = enumerable
?numbers=1&numbers=2
And probably a lot more formats. But all of these are framework/ implementation specific of whatever is receiving these calls as there is no official standard, and the same goes for something like
class Foo
{
public AuthEndPointArgs Args { get; set; }
}
Could be
?args.response_type=code&args.client_id=a%3Ab
And a bunch more different ways I can't be bothered to think off right now
one possibility is System.Text.Json.Serialization.JsonConverter<T> documented in How to customize property names and values with System.Text.Json
class AuthEndpointMarshaller : JsonConverter<AuthEndPointArgs>
{
public override AuthEndPointArgs? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
//TODO check typeToConvert
AuthEndPointArgs result = new();
if (reader.TokenType != JsonTokenType.StartObject)
throw new JsonException();
while (reader.Read())
{
if (reader.TokenType == JsonTokenType.EndObject)
{
return result;
}
if (reader.TokenType != JsonTokenType.PropertyName)
throw new JsonException();
string ? propertyName = reader.GetString();
reader.Read();
string? propertyValue = reader.GetString();
switch (propertyName)
{
case "response_type":
result.ResponseType = propertyValue;
break;
case "client_id":
result.ClientId = HttpUtility.UrlDecode(propertyValue);
break;
}
}
throw new JsonException();
}
public override void Write(Utf8JsonWriter writer, AuthEndPointArgs value, JsonSerializerOptions options)
{
value.ClientId = HttpUtility.UrlEncode(value.ClientId);
JsonSerializer.Serialize(writer,value);
}
}
You then call it using
string ConvertShallowObjectToQueryStringAlternate(AuthEndPointArgs source)
{
var options = new JsonSerializerOptions()
{
Converters =
{
new AuthEndpointMarshaller()
}
};
var jsonNode = JsonSerializer.SerializeToNode(source, options) ??
throw new InvalidOperationException(nameof(JsonSerializer.SerializeToNode));
return jsonNode.ToString();
}
or if you are looking for a quick solution
public record AuthEndPointArgs
{
// ...
public string ToQueryParams()
{
var sb = new StringBuilder($"?response_type={ResponseType}&client_id={HttpUtility.UrlEncode(ClientId)}");
return sb.ToString();
}
}

Newtonsoft.Json convertion xml document to json with array detection

what I am trying to do is to convert complex xml document to json format, and I am using Newtonsoft.Json to achieve my goal. and I have came across small - big problem. So for example
I have a model that looks like:
public class assets
{
public UInt32 id {get; set;}
public String providerName {get; set;}
public String provider {get; set;}
public String realm {get; set;}
public ICollection<unit> unit {get; set;}
}
My intention is that user will stream xml content to method that will change that xml to json and i will post it to API.
To simplify User is pasting simple xml (normal xml is far more complex, but basically it would looks like many levels of example bellow)
<assets xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="assets.xsd" providerName="myProviderName" provider="myProvider" realm="myCatalog">
<unit idKey="newGeo_63119679"></unit>
<unit idKey="newGeo_63119179"></unit>
</assets>
Json result will look like:
{"#providerName":"myProviderName","#provider":"myProvider","#realm":"myCatalog","unit":[{"#idKey":"newGeo_63119679"},{"#idKey":"newGeo_63119577"}]}
So service that does all the magic looks like:
public async ValueTask<string> AddAsset(string body)
{
XmlDocument doc = new XmlDocument();
doc.LoadXml(body);
string json = JsonConvert.SerializeXmlNode(doc, Formatting.None, true);
HttpResponseMessage response = await this._httpClient.PostAsJsonAsync("/Asset/create_asset", json);
string responseJson = await response.Content.ReadAsStringAsync();
return responseJson;
}
Well OK this part works, but when I remove from xml one unit node (so only one unit node is left), my result is:
{"#providerName":"myProviderName","#provider":"myProvider","#realm":"myCatalog","unit":{"#idKey":"newGeo_63119679"}}
And now so needed array to deserialize it to model is gone. I know I could manipulate xml attributes to add json:Array='true'.
But I was wondering if there is more complex solution for example JsonConverter that can take search for property in given type and check it if its collection and if so assign it as json collection. How can I bite this problem?
And Also as I checked SerializeXmlNode has no converter parameter.
Well, I found my answer, by mixing information from #dbc and my happy innovation:
public class Converter<TEntity> : JsonConverter<TEntity> where TEntity : class
{
private readonly IEnumerable<Type> _entityTypes =
Assembly.GetExecutingAssembly().GetReferencedAssemblies()
.SelectMany(assembly => Assembly.Load(assembly).GetTypes().Where(t => t.Namespace == MigrationService.EntitiesNameSpace));
public override bool CanWrite => false;
public override void WriteJson(JsonWriter writer, TEntity value, JsonSerializer serialize) =>
throw new NotImplementedException($"Write option is turned off for {nameof(CollectionConverter)} custom json converter");
public override TEntity ReadJson(JsonReader reader, Type objectType, TEntity existingValue, bool hasExistingValue, JsonSerializer serializer)
{
void SetSimpleTypes(JToken token, TEntity instance, PropertyInfo property)
{
var value = token[property.Name];
if(value == null) return;
var cast = Convert.ChangeType(value, property.PropertyType);
property.SetValue(instance, cast);
}
void SetClassTypes(JToken token, TEntity instance, PropertyInfo property)
{
var constructedConverterType = typeof(CollectionConverter<>).MakeGenericType(property.PropertyType);
if (Activator.CreateInstance(constructedConverterType) is not JsonConverter converter) return;
var propertyInstance = JsonConvert.DeserializeObject(token[property.Name].ToString(), property.PropertyType, converter);
property.SetValue(instance, propertyInstance);
}
void SetCollectionTypes(JToken token, TEntity instance, PropertyInfo property)
{
var constructedCollectionType = typeof(List<>).MakeGenericType(property.PropertyType.GetGenericArguments().Single());
var collectionInstance = Activator.CreateInstance(constructedCollectionType) as IList;
var value = token[property.Name];
void HandleSingleCollectionType(string body)
{
var propertyCollectionType = property.PropertyType.GetGenericArguments().Single();
var constructedConverterType = typeof(CollectionConverter<>).MakeGenericType(propertyCollectionType);
if (Activator.CreateInstance(constructedConverterType) is not JsonConverter converter) return;
var convertedInstance = JsonConvert.DeserializeObject(body, propertyCollectionType, converter);
collectionInstance?.Add(convertedInstance);
}
switch (value)
{
case JArray array:
foreach (var body in array)
{
HandleSingleCollectionType(body.ToString());
}
break;
case JObject:
HandleSingleCollectionType(value.ToString());
break;
default:
Debug.WriteLine($"Unknown or empty json token value for property {property.Name}");
break;
}
property.SetValue(instance, collectionInstance);
}
JToken token = JToken.Load(reader);
var instance = (TEntity)Activator.CreateInstance(typeof(TEntity));
var properties = instance?.GetType().GetProperties(BindingFlags.Public | BindingFlags.DeclaredOnly | BindingFlags.Instance);
if (properties == null) return default;
foreach (PropertyInfo property in properties)
{
try
{
if (property.PropertyType.IsGenericType && property.PropertyType.GetGenericTypeDefinition() == typeof(ICollection<>))
{
SetCollectionTypes(token, instance, property);
}
else if (this._entityTypes.Any(type => type == property.PropertyType) && property.PropertyType.IsClass)
{
SetClassTypes(token, instance, property);
}
else
{
SetSimpleTypes(token, instance, property);
}
}
catch (NullReferenceException ex)
{
Debug.WriteLine($"Null value for entity class property {property.Name}, exception: {ex}");
}
catch (FormatException ex)
{
Debug.WriteLine($"Can not convert value property {property.Name} in {instance.GetType().Name}, exception: {ex}");
}
catch (Exception ex)
{
Debug.WriteLine($"Undefined exception for {property.Name} exception: {ex}");
}
}
return instance;
}
}
It converts very complex xml structures based on class structure so if class structure says something is collection it will build collection, if its an object (I mean class) it will create instance of a class and so on. Additionally it uses itself to convert any child xml has.

Generic method to determine json is array or object in c#

My project contains too much calls to rest api and sometime i get json array and sometime json object.
Current I have to write same repeated code for each of call to determine the response json is array or object.
So i faced below errors because i dont know the incoming json type.
Cannot deserialize the current JSON array (e.g. [1,2,3]) into type 'userList' because the type requires a JSON object (e.g. {"name":"value"}) to deserialize correctly. To fix this error either change the JSON to a JSON object (e.g. {"name":"value"}) or change the deserialized type to an array or a type that implements a collection interface (e.g. ICollection, IList) like List that can be deserialized from a JSON array. JsonArrayAttribute can also be added to the type to force it to deserialize from a JSON array
OR
Cannot deserialize the current JSON object (e.g. {"name":"value"}) into type 'System.Collections.Generic.List`1[MvcSumit1.Models.User]' 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.
So to get rid of this problem I want generic method that can handle both of above scenarios.
I want generic method that can handle both of above scenarios.
The below generic method that can parse your incoming json to object or List<object>.
public class Utility
{
public static object JsonParser<T>(string json)
{
try
{
JToken jToken = JToken.Parse(json);
if (jToken is JArray)
return jToken.ToObject<List<T>>();
else if (jToken is JObject)
return jToken.ToObject<T>();
else
return "Unable to cast json to unknown type";
}
catch (JsonReaderException jex)
{
return jex.Message;
}
catch (Exception ex)
{
return ex.Message;
}
}
}
You can use above generic method like below. I created a console app for your demonstration purpose.
class Program
{
static void Main(string[] args)
{
var result = Utility.JsonParser<User>("You json either object or array");
if (result is List<User>)
{
var userList = result as List<User>;
userList.ForEach(user => Console.WriteLine($"Id: {user.Id}, Name: {user.Name}, Age: {user.Age}"));
}
else if (result is User)
{
var user = result as User;
Console.WriteLine($"Id: {user.Id}, Name: {user.Name}, Age: {user.Age}");
}
else if (result is string)
{
Console.WriteLine(result);
}
Console.ReadLine();
}
}
Sample class is used to deserialize your json.
public class User
{
public string Id { get; set; }
public string Name { get; set; }
public int Age { get; set; }
}
Output:
1) By using json with array of objects
string json1 = #"[{'Id':'1','Name':'Mike','Age':43},{'Id':'2','Name':'Anna','Age':56}]";
2) By using json with object only.
string json2 = #"{'Id':'3','Name':'John','Age':24}";
Alternative
The below generic method that can parse your incoming json to object or List<object> and return List<object>.
public class Utility
{
public static List<T> JsonParser<T>(string json)
{
JToken jToken = JToken.Parse(json);
if (jToken is JArray)
{
return jToken.ToObject<List<T>>();
}
else if (jToken is JObject)
{
List<T> lst = new List<T>();
lst.Add(jToken.ToObject<T>());
return lst;
}
else
return new List<T>();
}
}
You can use above generic method like below.
class Program
{
static void Main(string[] args)
{
var userList = Utility.JsonParser<User>("You json either object or array");
if (userList.Count > 0)
{
userList.ForEach(user => Console.WriteLine($"Id: {user.Id}, Name: {user.Name}, Age: {user.Age}"));
}
else
{
//Do code here if your list is empty
}
Console.ReadLine();
}
}
In case we have to deal with different types not just by Array or object, following is my approach:
(note I am using Newtonsoft.Json)
methods1
public bool TryDeserialize(string json, out object target, params Type[] types)
{
foreach (Type type in types)
{
try
{
target = JsonConvert.DeserializeObject(json, type);
return true;
}
catch (Exception)
{
}
}
target = null;
return false;
}
then use it like:
object obj = null;
Type[] types = new Type[] { typeof(TypeA), typeof(List<TypeB>) };
if (TryDeserialize(json, out obj, types))
{
if (obj is TypeA)
{
var r = obj as TypeA;
}
else
{
var r = obj as List<TypeB>;
}
}
or method 2
public bool TryDeserialize<T1, T2>(string json, out object target)
{
try
{
target = JsonConvert.DeserializeObject<T1>(json);
return true;
}
catch (Exception)
{
}
try
{
target = JsonConvert.DeserializeObject<T2>(json);
return true;
}
catch (Exception)
{
target = null;
return false;
}
}
and use it like:
object obj = null;
if (TryDeserialize<TypeA, List<TypeB>>(json, out obj))
{
if (obj is TypeA)
{
var r = obj as TypeA;
}
else
{
var r = obj as List<TypeB>;
}
}

JSON.Net cannot deserialize json array in custom JsonConverter

I've run into a confusing problem that i cannot seem to solve.
I'm using Json.Net and i've written a custom Json converter to handle a special case in my application.
The issue i've run into is in the deserialize or ReadJson method which is throwing an error when it tires to convert a JSON array into an array of strings:
The exact text of the error is: Unexpected token while deserializing object: PropertyName. Path 'RootPages', line 1, position 13.
As you can see from the inspector, the JProperty it is trying to deserialize (RootPages) has been parsed correctly and is valid JSON.
So i'm not entirely sure what is going on here, any enlightenment would be greatly appreciated..
If relevant, the original JSON string is as follows:
{
"RootPages": [
"TestItem1",
"TestItem2"
],
"Name": "root"
}
EDIT-CODE:
ReadJson Method:
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) {
if (PartialChildPageSerialization) {
var jsonObject = JObject.Load(reader);
var properties = jsonObject.Properties().ToList();
foreach (var property in objectType.GetProperties()) {
var type = property.PropertyType;
object value = null;
if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(ChildPageCollection<>)) {
var collection = Activator.CreateInstance(type);
var deserializedValue = properties.First(p => p.Name == property.Name).ToObject<string[]>();
type.GetMethod("PolulateFromSerializer").Invoke(collection, new object[] {deserializedValue});
value = collection;
}
else {
value = properties.First(p => p.Name == property.Name).ToObject(type);
}
property.SetValue(existingValue, value);
}
return existingValue;
}
return serializer.Deserialize(reader, objectType);
}
Snippet of the interesting part of ChildPageCollection:
public class ChildPageCollection<T> : IList<T> where T : DataPage
{
public string[] GetNames() => _internalNameList.ToArray();
internal void PolulateFromSerializer(string[] names) {
this.Clear();
_internalNameList.AddRange(names);
_hasFullList = false;
}
private void CheckFullList() {
if(!_hasFullList)
throw new InvalidOperationException("Collection has not been fully loaded, and full list is not avialable.");
}
private readonly List<T> _internalList = new List<T>();
private readonly List<string> _internalNameList = new List<string>();
private bool _hasFullList = true;
...
}
I expect this is because the first property is an object with a property called 'RootPages' which is a string[].
Unfortunately from the looks of your screenshots you are trying to turn an object into a string array.
This should work in the example you've given:
properties.First(p => p.Name == property.Name).Select(o => o.Children().Values<string>()).First().ToArray();
In place of:
properties.First(p => p.Name == property.Name).ToObject<string[]>();

Deserializing a null field that previously contained a value when it was serialized

I have read many posts regarding deserialization of nullable fields but have not run across the following scenario:
Serialize an object with a nullable field that contains a value ("nil" attribute is not added to the node because it contains a value).
Remove the value from the nullable field in the xml (this happens via client-side processing).
Deserialize the xml.
Step 3 throws an error because the serializer does not treat the empty value of the nullable field as a null value (because "nil=true" is not specified). It instead tries to convert the value to the field's data type (ex: Guid), which fails resulting in an error message that varies depending on the field's data type.
In the case of a Guid the error message is:
System.InvalidOperationException: There is an error in XML document ([line number], [column number]). ---> System.FormatException: Unrecognized Guid format.
I should note that the serialization / deserialization methods we use are framework methods that use generics.
I'm looking for an elegant and generic solution. The only feasible, generic solution I can think of is the following:
Convert the xml to an XDocument.
Use (less than desired) reflection to get all of the properties of the object that are reference types.
Add "nil=true" attribute to all nodes whose name is found in the list from #2 and has an empty value.
Use recursion to process each reference type in #2.
Note: Simply adding "nil=true" to all nodes that have an empty value will not work because the serializer will throw an error for value types that cannot be null.
[Edit] Code examples:
Sample data class
public class DummyData
{
public Guid? NullableGuid { get; set; }
}
Xml sent to client
<DummyData>
<NullableGuid>052ec82c-7322-4745-9ac1-20cc4e0f142d</NullableGuid>
</DummyData>
Xml returned from client (error)
<DummyData>
<NullableGuid></NullableGuid>
</DummyData>
Xml returned from client (desired result)
<DummyData>
<NullableGuid p2:nil="true" xmlns:p2="http://www.w3.org/2001/XMLSchema-instance"></NullableGuid>
</DummyData>
Here is the solution I came up with that pretty closely resembles my plan of attack described in the original question.
Disclaimer: It is not short and most likely does not cover every deserialization scenario but seems to get the job done.
public static T FromXml<T>(string xml)
{
string convertedXml = AddNilAttributesToNullableTypesWithNullValues(typeof(T), xml);
var reader = new StringReader(convertedXml);
var serializer = new XmlSerializer(typeof (T));
var data = (T) serializer.Deserialize(reader);
reader.Close();
return data;
}
private static string AddNilAttributesToNullableTypesWithNullValues(Type type, string xml)
{
string result;
if (!string.IsNullOrWhiteSpace(xml))
{
XDocument doc = XDocument.Parse(xml);
if (doc.Root != null)
AddNilAttributesToNullableTypesWithNullValues(doc.Root, type);
result = doc.ToString();
}
else
result = xml;
return result;
}
private static void AddNilAttributesToNullableTypesWithNullValues(XElement element, Type type)
{
if (type == null)
throw new ArgumentNullException("type");
if (element == null)
throw new ArgumentNullException("element");
//If this type can be null and it does not have a value, add or update nil attribute
//with a value of true.
if (IsReferenceOrNullableType(type) && string.IsNullOrEmpty(element.Value))
{
XAttribute existingNilAttribute = element.Attributes().FirstOrDefault(a => a.Name.LocalName == NIL_ATTRIBUTE_NAME);
if (existingNilAttribute == null)
element.Add(NilAttribute);
else
existingNilAttribute.SetValue(true);
}
else
{
//Process all of the objects' properties that have a corresponding child element.
foreach (PropertyInfo property in type.GetProperties())
{
string elementName = GetElementNameByPropertyInfo(property);
foreach (XElement childElement in element.Elements().Where(e =>
e.Name.LocalName.Equals(elementName)))
{
AddNilAttributesToNullableTypesWithNullValues(childElement, property.PropertyType);
}
}
//For generic IEnumerable types that have elements that correspond to the enumerated type,
//process the each element.
if (IsGenericEnumerable(type))
{
Type enumeratedType = GetEnumeratedType(type);
if (enumeratedType != null)
{
IEnumerable<XElement> enumeratedElements = element.Elements().Where(e =>
e.Name.LocalName.Equals(enumeratedType.Name));
foreach (XElement enumerableElement in enumeratedElements)
AddNilAttributesToNullableTypesWithNullValues(enumerableElement, enumeratedType);
}
}
}
}
private static string GetElementNameByPropertyInfo(PropertyInfo property)
{
string overrideElementName = property.GetCustomAttributes(true).OfType<XmlElementAttribute>().Select(xmlElementAttribute =>
xmlElementAttribute.ElementName).FirstOrDefault();
return overrideElementName ?? property.Name;
}
private static Type GetEnumeratedType(Type type)
{
Type enumerableType = null;
Type[] types = type.GetGenericArguments();
if (types.Length == 1)
enumerableType = types[0];
return enumerableType;
}
public static bool IsGenericEnumerable(Type type)
{
return type.IsGenericType && type.GetInterfaces().Any(i =>
i.IsGenericType && i.GetGenericTypeDefinition() == typeof(IEnumerable<>));
}
private static bool IsReferenceOrNullableType(Type type)
{
return !type.IsValueType || Nullable.GetUnderlyingType(type) != null;
}
private const string NIL_ATTRIBUTE_NAME = "nil";
private const string XML_SCHEMA_NAMESPACE = "http://www.w3.org/2001/XMLSchema-instance";
private static XAttribute NilAttribute
{
get
{
if (_nilAttribute == null)
{
XNamespace xmlSchemaNamespace = XNamespace.Get(XML_SCHEMA_NAMESPACE);
_nilAttribute = new XAttribute(xmlSchemaNamespace + NIL_ATTRIBUTE_NAME, true);
}
return _nilAttribute;
}
}

Categories

Resources