Gestion de l'interopérabilité JSON entre les API REST Java et C #
Je travaille actuellement avec 2 systèmes qui exposent l'interopérabilité via leurs propres API JSON RESTful. L'un est en C # avec JSON.NET et l'autre est Java Spring Boot Starter (Jackson JSON). J'ai un contrôle total sur les deux systèmes.
Les deux systèmes doivent transférer les données JSON avec la gestion des références. Alors que les deux frameworks de sérialisation JSON le prennent en charge, C # JSON.NET utilise "$id"et la "$ref"syntaxe pour signifier des références tandis que Jackson de Java utilise quelque chose de plus simple avec seulement "id".
Je connais beaucoup moins Java que C #, donc j'accepterais et comprendrais plus facilement toute solution permettant de faire fonctionner la gestion des références JSON dans les deux sens du côté C #. Comment puis-je faire interopérer ces deux systèmes avec les références JSON?
Documentation de gestion des références C # JSON.NET.
Exemple JSON provenant de Java Jackson
Notez qu'il est possible de marquer la propriété de classe que Jackson utilise comme référence. Dans ce cas, j'utilise la Idvariable car elle sera toujours localement unique au type.
{
"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
}
]
}
Réponses
Avant de lire , consultez mon autre approche de solution ici , cela peut être plus simple. Garder ce post aussi bien que je pense qu'il est informatif et peut être considéré comme une meilleure approche par certains.
Pourquoi est-ce compliqué?
Le problème ne concerne pas les noms de propriété de référence, car vous pouvez utiliser IReferenceResolverpour remplacer. Au lieu de cela, le problème est double:
La référence va d'une propriété d'un objet de la
Tasksliste à un objet de laResourcesliste. Ce n'est pas l'intention de laPreserveObjectReferencefonctionnalité. L'objectif était de ne pas répéter les objets de la même liste et d'éviter les références cycliques.La valeur dans la
Resourcepropriété de aTaskest un nombre au lieu d'unResourceobjet (qui n'aurait pas fonctionné de toute façon, en raison de l'élément 1 ci-dessus), par exemple
{
"Id": 0,
"Name": "Task 0",
"Resource": {
"$ref": 0
}
}
- Les identifiants et les références doivent être des chaînes, pas des nombres
Solution
Créez manuellement l'objet et faites correspondre manuellement les références:
- Nos 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; }
}
- Résolution du contrat:
/// <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
}
}
- Mise en œuvre:
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()}");
- Production:
Distinct resources: 2
Distinct tasks: 5
Distinct task resources: 2
.NET Fiddle
Peut-être une approche plus simple de ce que j'ai publié ici
Les raisons sont les mêmes, mais la solution est différente:
Utiliser une classe intermédiaire de sérialisation uniquement
(celui-ci a à la fois une solution de sérialisation et de désérialisation)
- Les DTO sont les mêmes:
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; }
}
- Classe de sérialisation provisoire uniquement:
/// <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
};
}
}
- Mise en œuvre:
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");
- Production:
Distinct resources: 2
Distinct tasks: 5
Distinct task resources: 2
Input and output are same: True
.NET Fiddle