Umgang mit JSON-Interop zwischen Java- und C # REST-APIs
Ich beschäftige mich derzeit mit 2 Systemen, die Interop über ihre eigenen RESTful JSON-APIs verfügbar machen. Einer ist in C # mit JSON.NET und einer ist Java Spring Boot Starter (Jackson JSON). Ich habe die volle Kontrolle über beide Systeme.
Beide Systeme müssen JSON-Daten mit Referenzbehandlung übertragen. Während beide JSON-Serialisierungs-Frameworks dies unterstützen, verwendet C # JSON.NET "$id"und "$ref"Syntax, um Referenzen zu kennzeichnen, während Javas Jackson nur etwas Einfacheres verwendet "id".
Ich bin mit Java viel weniger vertraut als mit C #, daher würde ich jede Lösung, mit der JSON-Ref-Handling auf der C # -Seite in beide Richtungen funktioniert, leichter akzeptieren und verstehen. Wie kann ich diese beiden Systeme dazu bringen, mit JSON-Refs zusammenzuarbeiten?
Dokumentation zur C # JSON.NET-Referenzbehandlung.
Beispiel JSON von Java Jackson
Beachten Sie, dass es möglich ist, zu markieren, welche Klasseneigenschaft Jackson als Referenz verwendet. In diesem Fall verwende ich die IdVariable, da sie für den Typ immer lokal eindeutig ist.
{
"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
}
]
}
Antworten
Vor dem Lesen , meinen anderen Lösungsansatz überprüft hier , kann es einfacher sein. Diesen Beitrag so zu halten, wie ich glaube, ist informativ und kann von einigen als besserer Ansatz angesehen werden.
Warum ist das kompliziert?
Das Problem sind nicht die Namen der Referenzeigenschaften, die Sie IReferenceResolverzum Überschreiben verwenden können. Stattdessen gibt es zwei Probleme:
Die Referenz stammt von einer Eigenschaft eines Objekts in der
TasksListe zu einem Objekt in derResourcesListe. Dies ist nicht die Absicht derPreserveObjectReferenceFunktion. Es war beabsichtigt, Objekte in derselben Liste nicht zu wiederholen und zyklische Verweise zu verhindern.Der Wert in der
ResourceEigenschaft von aTaskist eine Zahl anstelle einesResourceObjekts (was aufgrund von Punkt 1 oben sowieso nicht funktioniert hätte), z
{
"Id": 0,
"Name": "Task 0",
"Resource": {
"$ref": 0
}
}
- IDs und Referenzen müssen Zeichenfolgen sein, keine Zahlen
Lösung
Erstellen Sie das Objekt manuell und stimmen Sie die Referenzen manuell ab:
- Unsere DTOs:
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; }
}
- Vertragsabwicklung:
/// <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
}
}
- Implementierung:
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()}");
- Ausgabe:
Distinct resources: 2
Distinct tasks: 5
Distinct task resources: 2
.NET Geige
Möglicherweise eine einfachere Herangehensweise an das, was ich hier veröffentlicht habe
Die Gründe sind die gleichen, aber die Lösung ist anders:
Verwenden Sie eine Nur-Zwischen-Serialisierungsklasse
(Dieser hat sowohl Serialisierungs- als auch Deserialisierungslösung)
- DTOs sind die gleichen:
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; }
}
- Nur vorläufige Serialisierungsklasse:
/// <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
};
}
}
- Implementierung:
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");
- Ausgabe:
Distinct resources: 2
Distinct tasks: 5
Distinct task resources: 2
Input and output are same: True
.NET Geige