Berurusan dengan interop JSON antara Java dan C # REST API
Saat ini saya berurusan dengan 2 sistem yang mengekspos interop melalui API JSON RESTful mereka sendiri. Satu di C # dengan JSON.NET dan satu lagi adalah Java Spring Boot Starter (Jackson JSON). Saya memiliki kendali penuh atas kedua sistem.
Kedua sistem perlu mentransfer data JSON dengan penanganan referensi. Sementara kedua kerangka kerja serialisasi JSON mendukungnya, C # JSON.NET menggunakan "$id"dan "$ref"sintaks untuk menandakan referensi sementara Java's Jackson hanya menggunakan sesuatu yang lebih sederhana "id".
Saya jauh kurang akrab dengan Java daripada saya C # jadi saya akan lebih siap menerima dan memahami solusi apa pun untuk mendapatkan penanganan ref JSON bekerja dua arah di sisi C #. Bagaimana saya bisa mendapatkan kedua sistem ini untuk interop dengan referensi JSON?
Dokumentasi penanganan referensi C # JSON.NET.
Contoh JSON yang berasal dari Java Jackson
Perhatikan bahwa dimungkinkan untuk menandai properti kelas apa yang digunakan Jackson sebagai referensi. Dalam hal ini saya menggunakan Idvariabel karena akan selalu unik secara lokal untuk jenisnya.
{
"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
}
]
}
Jawaban
Sebelum membaca , lihat pendekatan solusi saya yang lain di sini , mungkin lebih sederhana. Menjaga posting ini dan saya percaya itu informatif dan mungkin dianggap pendekatan yang lebih baik oleh beberapa orang.
Mengapa ini rumit?
Masalahnya bukanlah nama properti referensi, untuk itu Anda bisa menggunakan IReferenceResolveruntuk mengganti. Sebaliknya, masalahnya ada dua:
Referensi berasal dari properti objek dalam
Tasksdaftar ke objek dalamResourcesdaftar. Ini bukan maksud dariPreserveObjectReferencefitur tersebut. Ini dimaksudkan untuk tidak mengulangi objek dalam daftar yang sama serta membantu mencegah referensi siklik.Nilai dalam
Resourceproperti aTaskadalah angka, bukanResourceobjek (yang bagaimanapun juga tidak akan berfungsi, karena item 1 di atas), mis.
{
"Id": 0,
"Name": "Task 0",
"Resource": {
"$ref": 0
}
}
- ID dan referensi harus berupa string, bukan angka
Larutan
Buat objek secara manual dan cocokkan referensi secara manual:
- DTO kami:
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; }
}
- Resolusi kontrak:
/// <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
}
}
- Penerapan:
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()}");
- Keluaran:
Distinct resources: 2
Distinct tasks: 5
Distinct task resources: 2
.NET Fiddle
Mungkin pendekatan yang lebih sederhana untuk apa yang saya terbitkan di sini
Alasannya sama, tetapi solusinya berbeda:
Gunakan kelas khusus serialisasi sementara
(yang ini memiliki solusi serialisasi dan deserialisasi)
- DTO sama:
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; }
}
- Kelas khusus serialisasi sementara:
/// <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
};
}
}
- Penerapan:
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");
- Keluaran:
Distinct resources: 2
Distinct tasks: 5
Distinct task resources: 2
Input and output are same: True
.NET Fiddle