Как обрабатывать ссылки на объекты в документе JSON с помощью Newtonsoft Json. NET? - PullRequest
1 голос
/ 07 февраля 2020

У меня есть json набор данных, который поставляется со стандартными полями данных и справочными полями. Это выглядит примерно так:

[
    {
        "id":1,
        "name":"Book",
        "description":"Something you can read"
    },
    {
        "id":2,
        "name":"newspaper",
        "description": {
            "ref":"0.description"
        }
    }
]

Это моя модель данных:

public class PhysicalObject {
    [Newtonsoft.Json.JsonProperty("id", Required = Newtonsoft.Json.Required.Default)]
    public int id;

    [Newtonsoft.Json.JsonProperty("name", Required = Newtonsoft.Json.Required.Default)]
    public string name;

    [Newtonsoft.Json.JsonProperty("description", Required = Newtonsoft.Json.Required.Default)]  // FIXED should have been description not desc
    public string desc;
}

Каждое свойство в файле json имеет определенный тип c, например int для id и string для description однако каждое свойство также может ссылаться на другое через ref. В этом случае description из id = 2 - это то же самое, что и id = 1

. Есть ли способ через обработку ошибок или создание некоторой резервной десериализации, которая может применяться Я хочу сериализовать ref?

Обратите внимание, что из-за других требований я должен использовать библиотеку Newtonsoft Json. NET для решения этой проблемы. Информация о других библиотеках или методах решения этой проблемы носит информативный характер, но, вероятно, не решит проблему.

Ответы [ 2 ]

2 голосов
/ 07 февраля 2020

Вам понадобится какой-то механизм, который разрешает эти ссылки.

Вот два подхода:

1. Использование встроенной обработки ссылок

Одним из таких механизмов является свойство PreserveReferencesHandling сериализатора Newtonsoft, которое выполняет именно то, что вы описываете, за исключением того, что вместо $id и $ref вместо id и ref.

Чтобы использовать это, вы можете преобразовать дерево JSON до его преобразования в типизированные объекты, сначала прочитав его в виде дерева JSON (используя JToken.Parse), затем, пройдя по этому дереву, заменив свойства id и ref на $id и $ref (поскольку промежуточное дерево JSON модифицируемо и динамически c по своей природе, вы можете сделать это легко).

Затем вы можете преобразовать это преобразованное дерево в ваши типизированные объекты, используя встроенный механизм разрешения ссылок, используя JObject.CreateReader, чтобы получить JsonReader для преобразованного дерева, которое вы может дать JsonSerializer.Deserialize<T>, чтобы дать ему команду десериализовать его в нужный тип.

T DeserializeJsonWithReferences<T>(string input) 
{
  var jsonTree = JToken.Parse(jsonString);
  TransformJsonTree(jsonTree);  // renames `id` and `ref` properties in-place
  var jsonReader = jsonTree.CreateReader();
  var jsonSerializer = new JsonSerializer() { 
    PreserveReferencesHandling = PreserveReferenceHandling.All 
  };
  var deserialized = jsonSerializer.Deserialize<T>(jsonReader);
  return deserialized;
}

void TransformJsonTree(JToken token)
{
  var container = token as JContainer;
  if (container == null) 
    return;

  foreach (propName in SpecialPropertyNames)  // {"id", "ref"}
  {
    objects = container
      .Descendants()
      .OfType<JObject>()
      .Where(x => x.ContainsKey(propName));

    foreach (obj in objects) 
    {
      obj["$" + propName] = obj[propName];
      obj.Remove(propName);
    }
  }
}

2. Прокрутка собственного слоя с эталонным разрешением

Более сложный подход, если вы хотите сделать это самостоятельно: вам нужно добавить свой собственный слой с эталонным разрешением, который преобразует дерево JSON до его преобразования в типизированный объекты.

Здесь вы можете начать с чтения потока JSON в виде дерева JSON. Затем вам нужно дважды пройти по этому дереву:

  • При первом обходе вы будете искать объекты со свойством id и записывать их в словарь (из *) 1045 * к объекту, содержащему его).

  • При втором обходе вы будете искать объекты со свойством ref и заменять эти ref-объекты соответствующим значением путем ищем указанный объект по его id в словаре, который вы создали ранее, затем перемещаетесь по его свойствам в соответствии с цепочкой свойств, описанной в значении ref. Например, если ref равен 3.address.city, вы будете искать объект с идентификатором 3, затем найдете значение его свойства address, а затем значение свойства city этого значения, и это будет быть окончательным значением ссылки.

После преобразования дерева JSON и замены всех ссылочных объектов их соответствующими ссылочными значениями вы можете преобразовать дерево JSON в типизированный объект.

В отношении кода, который будет точно таким же, как и в предыдущем примере, за исключением transformJsonTree, вместо простого переименования свойств id и ref, вам придется реализовать фактический поиск и logi эталонного разрешения c.

Это может выглядеть примерно так:

IDictionary<string, JToken> BuildIdMap(JContainer container)
{
  return container
    .Descendants()
    .OfType<JObject>()
    .Where(obj => obj.ContainsKey(IdPropertyName)
    .ToDictionary(obj => obj[IdPropertyName], obj => obj);
}

JToken LookupReferenceValue(string referenceString, IDictionary<string, JObject> idToObjectMap)
{
  var elements = referenceString.Split('.');
  var obj = idToObjectMap(elements[0]);

  for (int i = 1; i < elements.Length; i++) 
  {
    var elem = elements[i];
    switch (obj) 
    {
      case JArray jarr:
        obj = arr[elem];  // elem is a property name
        break;
      case JObject jobj:
        obj = jobj[int.Parse(elem)];  // elem is an array index
        break;
      default:
        throw Exception("You should throw a meaningful exception here"); 
    }
  }
}

void ResolveReferences(JContainer container, IDictionary<string, JObject> idToObjectMap)
{
  refObjects = container
    .Descendants()
    .OfType<JObject>()
    .Where(obj.Count == 1 && obj => obj.ContainsKey(RefPropertyName))

  foreach (var refObject in refObjects) 
  {
    referenceString = refObject[RefPropertyName];
    referencedValue = LookupReferenceValue(refObject, idToObjectMap)
    refObject.Replace(referencedValue);
  }
}

РЕДАКТИРОВАТЬ: Также взгляните на JToken.SelectToken, который позволяет перемещаться цепочка свойств из строки или JsonPath, что избавляет от многих проблем из вышеперечисленного (при условии, что ссылочный синтаксис в вашем документе соответствует поддерживаемому Newtonsoft, например, в отношении индексов массива).

JToken LookupReferenceValue(string referenceString, IDictionary<string, JObject> idToObjectMap)
{
  var parts = referenceString.Split('.', 1); // only split on first '.'
  var id = parts[0];
  var tokenPath = parts[1];
  var referencedObject = idToObjectMap[id];
  var referencedValue = referencedObject.SelectToken(tokenPath);
  return referencedValue;
}

Прошло много лет с тех пор, как я написал C#, поэтому прошу прощения за любые синтаксические ошибки или не-идиоматическое использование c. Но это общая идея.

1 голос
/ 11 февраля 2020

Вы можете предварительно загрузить JSON в иерархию JToken, затем использовать LINQ to JSON для замены объектов формы {"ref":"some.period-separated.path"} токенами, указанными в тропинка. Затем иерархия JToken может быть десериализована в вашу окончательную модель.

Следующий метод расширения делает свое дело:

public static partial class JsonExtensions
{
    const string refPropertyName = "ref";

    public static void ResolveRefererences(JToken root)
    {
        if (!(root is JContainer container))
            return;
        var refs = container.Descendants().OfType<JObject>().Where(o => IsRefObject(o)).ToList();
        Console.WriteLine(JsonConvert.SerializeObject(refs));
        foreach (var refObj in refs)
        {
            var path = GetRefObjectValue(refObj);
            var original = ResolveRef(root, path);
            if (original != null)
                refObj.Replace(original);
        }
    }

    static bool IsRefObject(JObject obj)
    {
        return GetRefObjectValue(obj) != null;
    }

    static string GetRefObjectValue(JObject obj)
    {
        if (obj.Count == 1)
        {
            var refValue = obj[refPropertyName];
            if (refValue != null && refValue.Type == JTokenType.String)
            {
                return (string)refValue;
            }
        }
        return null;
    }

    static JToken ResolveRef(JToken token, string path)
    {
        // TODO: determine whether it is possible for a property name to contain a '.' character, and if so, how the path will look.
        var components = path.Split('.'); 

        foreach (var component in components)
        {
            if (token is JObject obj)
                token = obj[component];
            else if (token is JArray array)
                token = token[int.Parse(component, NumberFormatInfo.InvariantInfo)];
            else
                // Or maybe just return null?
                throw new JsonException("Unexpected token type.");
        }
        return token;
    }
} 

Тогда вы будете использовать его следующим образом:

// Load into intermediate JToken hierarchy; do not perform DateTime recognition yet.
var root = JsonConvert.DeserializeObject<JToken>(jsonString, new JsonSerializerSettings { DateParseHandling = DateParseHandling.None });

// Replace {"ref": "...") objects with their references.
JsonExtensions.ResolveRefererences(root);

// Deserialize directly to final model.  DateTime recognition should get performed now.
var list = root.ToObject<List<PhysicalObject>>();

Примечания:

  1. Это решение не пытается сохранить ссылки, то есть заставить десериализованный {"ref":"some.period-separated.path"} ссылаться на тот же экземпляр, что и десериализованный оригинал. В то время как Json. NET имеет функциональность для сохранения ссылок на объекты через "$ref" и "$id" свойств, у него есть несколько ограничений, включая:

    • It не обрабатывает ссылки на примитивы, только объекты и массивы.

    • Он не допускает прямые ссылки, только обратные ссылки. Из вопроса неясно, могут ли свойства "ref" в вашем JSON ссылаться на значения, приведенные далее в документе.


    Эти ограничения усложнят преобразование показанного эталонного синтаксиса. в вопросе о синтаксисе Json. NET.

  2. Хорошая идея - отложить DateTime распознавание до окончательной десериализации. Если ваша модель имеет свойства string, значения которых JSON могут выглядеть как даты ISO 8601, то преждевременное распознавание даты может привести к изменению строковых значений.

Демонстрационная скрипка здесь .

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...