Eccezione di riferimento circolare durante la serializzazione di un object contenente un JToken in XML nell’API Web

Nel mio database, ho una tabella con un sacco di colonne e una di esse contiene una stringa JSON (non ho alcun controllo su questo). Qualcosa come questo:

Name Age ExtraData ---- --- ------------------ Bob 31 {c1: "1", c2: "2"} <-- string with JSON 

L’endpoint dell’API Web deve restituire XML o JSON in base alle intestazioni Accept nella richiesta. Qualcosa come questo:

JSON:

 { "Name": "Bob", "Age": 31, "ExtraData": { "c1": 1, "c2": 2 } } 

XML:

  Bob 31  1 2   

Per fare questo, ho creato una class in C # come questa:

 public class Person { public string Name { get; set; } public int Age { get; set; } public Object ExtraData { get; set; } } 

Quando analizzo i dati dal database, ExtraData gli ExtraData questo modo:

 personInstance.ExtraData = JsonConvert.DeserializeObject(personTableRow.ExtraData); 

Quando l’API Web restituisce JSON, tutto funziona come previsto.

Quando l’API Web restituisce XML, fornisce un’eccezione:

Il tipo ‘ObjectContent`1’ non è riuscito a serializzare il corpo della risposta per il tipo di contenuto ‘application / xml; charset = utf-8′ .

L’eccezione interna, è qualcosa di simile (non è in inglese):

Newtonsoft.Json.Linq.JToken ha un riferimento circolare e non è supportato. (O tipo ‘Newtonsoft.Json.Linq.JToken’ è un contrato di papà di colonia con ricorsiva che non contiene niente. Considere modificar a definição da coleção ‘Newtonsoft.Json.Linq.JToken’ para remover referências a si mesma.)

Esiste un modo per analizzare i dati JSON su un object senza riferimento circolare?

Hai incontrato una limitazione di XmlSerializer . Quando deserializza un object JSON (che è un insieme non ordinato di coppie nome / valore circondate da parentesi graffe) in object ac #, Json.NET crea un object di tipo JObject e sfortunatamente XmlSerializer non sa come serializzare un JObject . In particolare cade in una ricorsione infinita cercando di serializzare i figli di JToken.Parent . Pertanto, è necessario convertire l’ object ExtraData sottostante object ExtraData in un tipo che XmlSerializer può gestire.

Tuttavia, non è ovvio quale tipo utilizzare, dal momento che:

  • Il tipo di c # più naturale con cui rappresentare un object JSON è un dizionario e XmlSerializer non supporta i dizionari .

  • XmlSerializer funziona per rilevamento di tipo statico . Tutti i sottotipi polimorfici object che possono essere incontrati devono essere dichiarati tramite [XmlInclude(typof(T))] . Tuttavia, se ciò è fatto, l’XML includerà il tipo effettivo come un attributo xsi:type che non sembra voler nel tuo XML.

Quello che puoi fare è sfruttare la funzionalità [XmlAnyElement] per creare una proprietà surrogata che converte l’ object ExtraData da e verso un XElement usando XmlNodeConverter di XmlNodeConverter :

 public class Person { public string Name { get; set; } public int Age { get; set; } [XmlIgnore] [JsonProperty] public object ExtraData { get; set; } [XmlAnyElement("ExtraData")] [JsonIgnore] public XElement ExtraDataXml { get { return JsonExtensions.SerializeExtraDataXElement("ExtraData", ExtraData); } set { ExtraData = JsonExtensions.DeserializeExtraDataXElement("ExtraData", value); } } } public static class JsonExtensions { public static XElement SerializeExtraDataXElement(string name, object extraData) { if (extraData == null) return null; var token = JToken.FromObject(extraData); if (token is JValue) { return new XElement(name, (string)token); } else if (token is JArray) { return new JObject(new JProperty(name, token)).ToXElement(false, name, true); } else { return token.ToXElement(false, name, true); } } public static object DeserializeExtraDataXElement(string name, XElement element) { object extraData; if (element == null) extraData = null; else { extraData = element.ToJToken(true, name, true); if (extraData is JObject) { var obj = (JObject)extraData; if (obj.Count == 1 && obj.Properties().First().Name == name) extraData = obj.Properties().First().Value; } if (extraData is JValue) { extraData = ((JValue)extraData).Value; } } return extraData; } public static XElement ToXElement(this JToken obj, bool omitRootObject, string deserializeRootElementName, bool writeArrayAttribute) { if (obj == null) return null; using (var reader = obj.CreateReader()) { var converter = new Newtonsoft.Json.Converters.XmlNodeConverter() { OmitRootObject = omitRootObject, DeserializeRootElementName = deserializeRootElementName, WriteArrayAttribute = writeArrayAttribute }; var jsonSerializer = JsonSerializer.CreateDefault(new JsonSerializerSettings { Converters = { converter } }); return jsonSerializer.Deserialize(reader); } } public static JToken ToJToken(this XElement xElement, bool omitRootObject, string deserializeRootElementName, bool writeArrayAttribute) { // Convert to Linq to XML JObject var settings = new JsonSerializerSettings { Converters = { new XmlNodeConverter { OmitRootObject = omitRootObject, DeserializeRootElementName = deserializeRootElementName, WriteArrayAttribute = writeArrayAttribute } } }; var root = JToken.FromObject(xElement, JsonSerializer.CreateDefault(settings)); return root; } } 

Utilizzando la class di cui sopra, posso deserializzare il tuo JSON e serializzarlo in XML con il seguente risultato:

  Bob 31  1 2   

Si noti che ci sono incoerenze tra JSON e XML che causano problemi:

  • I valori primitivi JSON sono “leggermente” tipizzati (come stringa, numero, booleano o null) mentre il testo XML è completamente non tipizzato. Pertanto, i valori numerici (e le date) nel JSON vengono convertiti in XML come stringhe.

  • XML non ha concetto di un array. Pertanto, il JSON il cui contenitore radice è un array richiede un elemento radice sintetico da aggiungere durante la serializzazione. Questo aggiunge un po ‘di odore di codice durante il processo di conversione.

  • XML deve avere un singolo elemento radice mentre JSON valido può essere costituito da un valore primitivo, come una stringa. Ancora una volta è richiesto un elemento sintetico di radice durante la conversione.

Qui prototipo leggermente testato, dove dimostro che il codice funziona per ExtraData che è un object JSON, una matrice di stringhe, una stringa singola e un valore null .