การจัดการกับ JSON interop ระหว่าง Java และ C # REST API
ตอนนี้ฉันกำลังจัดการกับ 2 ระบบที่เปิดเผยการทำงานร่วมกันผ่าน RESTful JSON API ของตัวเอง หนึ่งอยู่ใน C # กับ JSON.NET และอีกอันคือ Java Spring Boot Starter (Jackson JSON) ฉันสามารถควบคุมทั้งสองระบบได้อย่างสมบูรณ์
ทั้งสองระบบจำเป็นต้องถ่ายโอนข้อมูล JSON ด้วยการจัดการอ้างอิง ขณะที่ทั้ง JSON กรอบอนุกรมสนับสนุนมัน, C # JSON.NET ใช้"$id"และ"$ref"ไวยากรณ์ที่มีความหมายการอ้างอิงในขณะที่ของ Java "id"แจ็คสันใช้บางสิ่งบางอย่างชัดมีเพียง
ฉันคุ้นเคยกับ Java น้อยกว่าฉัน C # มากดังนั้นฉันจึงพร้อมที่จะยอมรับและเข้าใจวิธีแก้ปัญหาในการจัดการการอ้างอิง JSON ที่ทำงานได้ทั้งสองวิธีในด้าน C # ฉันจะให้ทั้งสองระบบนี้ทำงานร่วมกับ JSON refs ได้อย่างไร
เอกสารคู่มือการจัดการอ้างอิง C # JSON.NET
ตัวอย่าง JSON ที่มาจาก Java Jackson
โปรดทราบว่าเป็นไปได้ที่จะทำเครื่องหมายว่าคุณสมบัติของคลาสใดที่ 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เพื่อลบล้างได้ แต่ปัญหากลับเป็นสองเท่า:
การอ้างอิงมาจากคุณสมบัติของวัตถุใน
TasksรายการไปยังวัตถุในResourcesรายการ นี่ไม่ใช่ความตั้งใจของPreserveObjectReferenceคุณลักษณะ มีวัตถุประสงค์เพื่อไม่ให้วัตถุซ้ำในรายการเดียวกันรวมทั้งช่วยป้องกันการอ้างอิงแบบวนซ้ำค่าใน
Resourceคุณสมบัติของ aTaskเป็นตัวเลขแทนที่จะเป็นResourceวัตถุ (ซึ่งจะไม่ได้ผลใด ๆ เนื่องจากรายการ 1 ด้านบน) เช่น
{
"Id": 0,
"Name": "Task 0",
"Resource": {
"$ref": 0
}
}
- ID และการอ้างอิงต้องเป็นสตริงไม่ใช่ตัวเลข
สารละลาย
สร้างวัตถุด้วยตนเองและจับคู่ข้อมูลอ้างอิงด้วยตนเอง:
- 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; }
}
- การแก้สัญญา:
/// <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
}
}
- การนำไปใช้:
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()}");
- เอาท์พุต:
Distinct resources: 2
Distinct tasks: 5
Distinct task resources: 2
.NET ซอ
อาจเป็นแนวทางที่ง่ายกว่าสำหรับสิ่งที่ฉันเผยแพร่ที่นี่
เหตุผลเหมือนกัน แต่วิธีแก้ปัญหาแตกต่างกัน:
ใช้คลาสอนุกรมระหว่างกาลเท่านั้น
(อันนี้มีทั้งวิธีการทำให้เป็นอนุกรมและการแยกสารออกจากกัน)
- 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; }
}
- คลาสอนุกรมระหว่างกาลเท่านั้น:
/// <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
};
}
}
- การนำไปใช้:
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");
- เอาท์พุต:
Distinct resources: 2
Distinct tasks: 5
Distinct task resources: 2
Input and output are same: True
.NET ซอ