簡體   English   中英

處理 Java 和 C# REST API 之間的 JSON 互操作

[英]Dealing with JSON interop between Java and C# REST APIs

我目前正在處理 2 個通過他們自己的 RESTful JSON API 公開互操作的系統。 一種是帶有 JSON.NET 的 C#,一種是 Java Spring Boot Starter (Jackson JSON)。 我可以完全控制這兩個系統。

兩個系統都需要通過引用處理來傳輸 JSON 數據。 雖然兩個 JSON 序列化框架都支持它,但 C# JSON.NET 使用"$id""$ref"語法來表示引用,而 Java 的 Jackson 使用更簡單的東西,只有"id"

與 C# 相比,我對 Java 的熟悉程度要低得多,因此我更願意接受和理解有關在 C# 端以兩種方式處理 JSON ref 的任何解決方案。 我怎樣才能讓這兩個系統與 JSON refs 互操作?

C# JSON.NET 參考處理文檔。

來自 Java Jackson 的示例 JSON

請注意,可以標記 Jackson 用作參考的類屬性。 在這種情況下,我使用Id變量,因為它對於該類型始終是本地唯一的。

{
  "Resources": [
      {
          "Id": 0,
          "Name": "Resource 0"
      },
      {
          "Id": 1,
          "Name": "Resource 1"
      }
  ],
  "Tasks": [
      {
          "Id": 0,
          "Name": "Task 0",
          "Resource": 0
      },
      {
          "Id": 1,
          "Name": "Task 1",
          "Resource": 1
      },
      {
          "Id": 2,
          "Name": "Task 2",
          "Resource": 0
      },
      {
          "Id": 3,
          "Name": "Task 3",
          "Resource": 1
      },
      {
          "Id": 4,
          "Name": "Task 4",
          "Resource": 0
      }
  ]
}

在閱讀之前,請在此處查看我的其他解決方案方法,它可能更簡單。 保留這篇文章以及我認為它提供了豐富的信息,並且可能會被某些人認為是更好的方法。


為什么這很復雜?

問題不在於引用屬性名稱,因為您可以使用IReferenceResolver進行覆蓋。 相反,問題有兩個方面:

  1. 引用是從Tasks列表中對象的屬性到Resources列表中的對象。 這不是PreserveObjectReference功能的意圖。 它旨在不重復同一列表中的對象,並有助於防止循環引用。

  2. TaskResource屬性中的值是一個數字而不是一個Resource對象(由於上面的第 1 項,它無論如何都不起作用),例如

{
  "Id": 0,
  "Name": "Task 0",
  "Resource": {
    "$ref": 0
  }
}
  1. ID 和引用必須是字符串,而不是數字

解決方案

手動構建對象並手動匹配引用:

  1. 我們的 DTO:
public class Dto
{
    public Resource[] Resources { get; set; }
    public Task[] Tasks { get; set; }
}

public class Resource
{
    public long Id { get; set; }
    public string Name { get; set; }
}

public class Task
{
    public long Id { get; set; }
    public string Name { get; set; }
    public Resource Resource { get; set; }
}
  1. 合同決議:
/// <summary>
/// This is to resolve the Resource resolver for the Task
/// </summary>
internal class TaskResourceContractResolver : DefaultContractResolver
{
    private readonly IDictionary<long, Resource> _resources;

    public TaskResourceContractResolver(IDictionary<long, Resource> resources) => this._resources = resources;

    #region Overrides of DefaultContractResolver
    protected override JsonProperty CreateProperty(MemberInfo member, MemberSerialization memberSerialization)
    {
        var property = base.CreateProperty(member, memberSerialization);
        if (property.DeclaringType != typeof(Task) || property.PropertyName != nameof(Task.Resource))
            return property;

        property.Converter = new TaskResourceConverter(this._resources);
        property.IsReference = true;
        property.ValueProvider = new CurrentValueGetterValueProvider();
        return property;
    }
    #endregion Overrides of DefaultContractResolver

    /// <summary>
    /// This is to resolve the Resource for the Task
    /// </summary>
    private class TaskResourceConverter : JsonConverter<Task>
    {
        private readonly IDictionary<long, Resource> _resources;

        public TaskResourceConverter(IDictionary<long, Resource> resources) => this._resources = resources;

        #region Overrides of JsonConverter
        public override void WriteJson(JsonWriter writer, Task value, JsonSerializer serializer) => throw new NotImplementedException();
        public override Task ReadJson(JsonReader reader, Type objectType, Task existingValue, bool hasExistingValue, JsonSerializer serializer)
        {
            if (reader.Value is Resource resource) existingValue.Resource = resource;
            else if (reader.Value is long resourceRef)
            {
                if (!this._resources.TryGetValue(resourceRef, out resource)) throw new Exception($"Invalid resource reference '{resourceRef}'");
                existingValue.Resource = resource;
            }
            else throw new Exception($"Invalid resource reference '{reader.Value}'");

            return existingValue;
        }
        #endregion Overrides of JsonConverter
    }

    /// <summary>
    /// This is so we get the value of Task object to be set
    /// </summary>
    private class CurrentValueGetterValueProvider : IValueProvider
    {
        #region Implementation of IValueProvider
        public void SetValue(object target, object value) => throw new NotImplementedException();
        public object GetValue(object target) => target;
        #endregion Implementation of IValueProvider
    }
}
  1. 執行:
var input = Encoding.UTF8.GetString(Properties.Resources.input); // the posted Java-outputted JSON
var parsed = JObject.Parse(input);

var resources = parsed[nameof(Dto.Resources)]?.Children()
    .Select(token => token.ToObject<Resource>())
    .ToDictionary(r => r!.Id);

var serializer = new JsonSerializer() { ContractResolver = new TaskResourceContractResolver(resources) };

var dto = new Dto
{
    Resources = resources?.Values.ToArray(),
    Tasks = parsed[nameof(Dto.Tasks)]?.Children()
        .Select(token => token.ToObject<Task>(serializer))
        .ToArray()
};

Console.WriteLine($@"Distinct resources: {dto.Resources?.Distinct().Count()}");
Console.WriteLine($@"Distinct tasks: {dto.Tasks?.Distinct().Count()}");
Console.WriteLine($@"Distinct task resources: {dto.Tasks?.Select(t => t.Resource).Distinct().Count()}");
  1. 輸出:
Distinct resources: 2
Distinct tasks: 5
Distinct task resources: 2

.NET 小提琴

可能是我在此處發布的更簡單的方法

原因相同,但解決方案不同:

使用臨時僅序列化類

(這個有序列化和反序列化兩種方案)

  1. DTO 是相同的:
public class Dto
{
    public Resource[] Resources { get; set; }
    public Task[] Tasks { get; set; }
}

public class Resource
{
    public long Id { get; set; }
    public string Name { get; set; }
}

public class Task
{
    public long Id { get; set; }
    public string Name { get; set; }
    public Resource Resource { get; set; }
}
  1. 僅臨時序列化類:
/// <summary>
/// Helper class for Dto serialization
/// </summary>
internal class DtoSerializationHelper
{
    public Resource[] Resources { get; set; }

    /// <summary>
    /// To be used by application code (not for 
    /// </summary>
    [JsonIgnore]
    public Task[] Tasks { get; set; }

    /// <summary>
    /// Used by serializer
    /// </summary>
    [JsonProperty(nameof(Tasks))]
    private TaskSerializationHelper[] SerializationTasks { get; set; }

    [OnDeserialized]
    private void OnDeserialized(StreamingContext context)
    {
        var resourceLookup = this.Resources.ToDictionary(r => r.Id);
        this.Tasks = this.SerializationTasks.Select(t => t.ToTask(resourceLookup)).ToArray();
    }

    [OnSerializing]
    private void OnSerializing(StreamingContext context)
    {
        this.SerializationTasks = this.Tasks?.Select(t => new TaskSerializationHelper(t)).ToArray();
    }

    /// <summary>
    /// Converts from the helper to the Dto when casting
    /// </summary>
    /// <param name="helper"></param>
    public static implicit operator Dto(DtoSerializationHelper helper) => new Dto
    {
        Resources = helper.Resources,
        Tasks = helper.Tasks
    };

    /// <summary>
    /// Converts from the Dto to the helper when casting
    /// </summary>
    /// <param name="dto"></param>
    public static explicit operator DtoSerializationHelper(Dto dto) => new DtoSerializationHelper
    {
        Resources = dto.Resources,
        Tasks = dto.Tasks
    };

    /// <summary>
    /// A Task serialization helper class
    /// </summary>
    private class TaskSerializationHelper
    {
        public TaskSerializationHelper() { }

        public TaskSerializationHelper(Task task) : this()
        {
            this.Id = task.Id;
            this.Name = task.Name;
            this.Resource = task.Resource.Id;
        }

        public long Id { get; set; }
        public string Name { get; set; }
        public long Resource { get; set; }

        public Task ToTask(IDictionary<long, Resource> resourceLookup) =>
            new Task
            {
                Id = this.Id,
                Name = this.Name,
                Resource = resourceLookup is null || !resourceLookup.TryGetValue(this.Resource, out var resource)
                    ? throw new Exception($"Invalid resource {this.Resource}")
                    : resource
            };
    }
}
  1. 執行:
var input = Encoding.UTF8.GetString(Properties.Resources.input); // the posted Java-outputted JSON

var dtoSerializationHelper = JsonConvert.DeserializeObject<DtoSerializationHelper>(input);
var dto = (Dto)dtoSerializationHelper;

var deserializationResults = new
{
    distinctResources = dto.Resources?.Distinct().Count(),
    distinctTasks = dto.Tasks?.Distinct().Count(),
    distinctTaskResources = dto.Tasks?.Select(t => t.Resource).Distinct().Count()
};

Console.WriteLine($@"Distinct resources: {deserializationResults.distinctResources}");
Console.WriteLine($@"Distinct tasks: {deserializationResults.distinctTasks}");
Console.WriteLine($@"Distinct task resources: {deserializationResults.distinctTaskResources}");

if (deserializationResults.distinctResources != 2 ||
    deserializationResults.distinctTasks != 5 ||
    deserializationResults.distinctTaskResources != 2) throw new Exception("Deserialization failed");

Console.WriteLine();

var output = JsonConvert.SerializeObject((DtoSerializationHelper)dto);
var serializationResult = output == input;

Console.WriteLine($@"Input and output are same: {serializationResult}");
if (serializationResult) return;

Console.WriteLine($@"Output: {output}");
throw new Exception("Serialization failed");
  1. 輸出:
Distinct resources: 2
Distinct tasks: 5
Distinct task resources: 2

Input and output are same: True

.NET 小提琴

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM