¿Puedo especificar una ruta en un atributo para asignar una propiedad de mi clase a una propiedad secundaria en mi JSON?
Hay un código (que no puedo cambiar) que usa Newtonsoft.Json DeserializeObject<T>(strJSONData)
para tomar datos de una solicitud web y convertirlos en un objeto de clase (puedo cambiar la clase). Al decorar las propiedades de mi clase, [DataMember(Name = "raw_property_name")]
puedo asignar los datos JSON sin procesar a la propiedad correcta en mi clase. ¿Hay alguna manera de asignar la propiedad secundaria de un objeto complejo JSON a una propiedad simple? He aquí un ejemplo:
{
"picture":
{
"id": 123456,
"data":
{
"type": "jpg",
"url": "http://www.someplace.com/mypicture.jpg"
}
}
}
No me importa el resto del objeto de imagen excepto la URL, por lo que no quiero configurar un objeto complejo en mi clase de C#. Realmente solo quiero algo como:
[DataMember(Name = "picture.data.url")]
public string ProfilePicture { get; set; }
es posible?
Bueno, si solo necesita una propiedad adicional, un enfoque simple es analizar su JSON en a JObject
, usarlo ToObject()
para completar su clase desde JObject
y luego usarlo SelectToken()
para incorporar la propiedad adicional.
Entonces, suponiendo que su clase se viera así:
class Person
{
[JsonProperty("name")]
public string Name { get; set; }
[JsonProperty("age")]
public string Age { get; set; }
public string ProfilePicture { get; set; }
}
Podrías hacer esto:
string json = @"
{
""name"" : ""Joe Shmoe"",
""age"" : 26,
""picture"":
{
""id"": 123456,
""data"":
{
""type"": ""jpg"",
""url"": ""http://www.someplace.com/mypicture.jpg""
}
}
}";
JObject jo = JObject.Parse(json);
Person p = jo.ToObject<Person>();
p.ProfilePicture = (string)jo.SelectToken("picture.data.url");
Violín: https://dotnetfiddle.net/7gnJCK
Si prefiere una solución más sofisticada, puede personalizarla JsonConverter
para permitir que el JsonProperty
atributo se comporte como lo describe. El convertidor necesitaría operar a nivel de clase y utilizar algo de reflexión combinada con la técnica anterior para completar todas las propiedades. Así es como podría verse en el código:
class JsonPathConverter : JsonConverter
{
public override object ReadJson(JsonReader reader, Type objectType,
object existingValue, JsonSerializer serializer)
{
JObject jo = JObject.Load(reader);
object targetObj = Activator.CreateInstance(objectType);
foreach (PropertyInfo prop in objectType.GetProperties()
.Where(p => p.CanRead && p.CanWrite))
{
JsonPropertyAttribute att = prop.GetCustomAttributes(true)
.OfType<JsonPropertyAttribute>()
.FirstOrDefault();
string jsonPath = (att != null ? att.PropertyName : prop.Name);
JToken token = jo.SelectToken(jsonPath);
if (token != null && token.Type != JTokenType.Null)
{
object value = token.ToObject(prop.PropertyType, serializer);
prop.SetValue(targetObj, value, null);
}
}
return targetObj;
}
public override bool CanConvert(Type objectType)
{
// CanConvert is not called when [JsonConverter] attribute is used
return false;
}
public override bool CanWrite
{
get { return false; }
}
public override void WriteJson(JsonWriter writer, object value,
JsonSerializer serializer)
{
throw new NotImplementedException();
}
}
Para demostrarlo, supongamos que el JSON ahora tiene el siguiente aspecto:
{
"name": "Joe Shmoe",
"age": 26,
"picture": {
"id": 123456,
"data": {
"type": "jpg",
"url": "http://www.someplace.com/mypicture.jpg"
}
},
"favorites": {
"movie": {
"title": "The Godfather",
"starring": "Marlon Brando",
"year": 1972
},
"color": "purple"
}
}
...y te interesa la película favorita de la persona (título y año) y su color favorito además de la información anterior. Primero marcaría su clase de destino con un [JsonConverter]
atributo para asociarla con el convertidor personalizado, luego usaría [JsonProperty]
atributos en cada propiedad, especificando la ruta de propiedad deseada (distingue entre mayúsculas y minúsculas) como nombre. Las propiedades de destino tampoco tienen que ser primitivas: puede usar una clase secundaria como lo hice aquí Movie
(y observe que no se Favorites
requiere ninguna clase intermedia).
[JsonConverter(typeof(JsonPathConverter))]
class Person
{
[JsonProperty("name")]
public string Name { get; set; }
[JsonProperty("age")]
public int Age { get; set; }
[JsonProperty("picture.data.url")]
public string ProfilePicture { get; set; }
[JsonProperty("favorites.movie")]
public Movie FavoriteMovie { get; set; }
[JsonProperty("favorites.color")]
public string FavoriteColor { get; set; }
}
// Don't need to mark up these properties because they are covered by the
// property paths in the Person class
class Movie
{
public string Title { get; set; }
public int Year { get; set; }
}
Con todos los atributos implementados, puede deserializar normalmente y debería "simplemente funcionar":
Person p = JsonConvert.DeserializeObject<Person>(json);
Violín: https://dotnetfiddle.net/Ljw32O
La respuesta marcada no está 100% completa ya que ignora cualquier IContractResolver que pueda estar registrado, como CamelCasePropertyNamesContractResolver, etc.
Además, devolver falso para can convert evitará otros casos de usuarios, así que lo cambié areturn objectType.GetCustomAttributes(true).OfType<JsonPathConverter>().Any();
Aquí está la versión actualizada: https://dotnetfiddle.net/F8C8U8
También eliminé la necesidad de establecer una JsonProperty
propiedad como se ilustra en el enlace.
Si por alguna razón el enlace anterior muere o explota, también incluyo el siguiente código:
public class JsonPathConverter : JsonConverter
{
/// <inheritdoc />
public override object ReadJson(
JsonReader reader,
Type objectType,
object existingValue,
JsonSerializer serializer)
{
JObject jo = JObject.Load(reader);
object targetObj = Activator.CreateInstance(objectType);
foreach (PropertyInfo prop in objectType.GetProperties().Where(p => p.CanRead && p.CanWrite))
{
JsonPropertyAttribute att = prop.GetCustomAttributes(true)
.OfType<JsonPropertyAttribute>()
.FirstOrDefault();
string jsonPath = att != null ? att.PropertyName : prop.Name;
if (serializer.ContractResolver is DefaultContractResolver)
{
var resolver = (DefaultContractResolver)serializer.ContractResolver;
jsonPath = resolver.GetResolvedPropertyName(jsonPath);
}
if (!Regex.IsMatch(jsonPath, @"^[a-zA-Z0-9_.-]+$"))
{
throw new InvalidOperationException($"JProperties of JsonPathConverter can have only letters, numbers, underscores, hiffens and dots but name was ${jsonPath}."); // Array operations not permitted
}
JToken token = jo.SelectToken(jsonPath);
if (token != null && token.Type != JTokenType.Null)
{
object value = token.ToObject(prop.PropertyType, serializer);
prop.SetValue(targetObj, value, null);
}
}
return targetObj;
}
/// <inheritdoc />
public override bool CanConvert(Type objectType)
{
// CanConvert is not called when [JsonConverter] attribute is used
return objectType.GetCustomAttributes(true).OfType<JsonPathConverter>().Any();
}
/// <inheritdoc />
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
var properties = value.GetType().GetRuntimeProperties().Where(p => p.CanRead && p.CanWrite);
JObject main = new JObject();
foreach (PropertyInfo prop in properties)
{
JsonPropertyAttribute att = prop.GetCustomAttributes(true)
.OfType<JsonPropertyAttribute>()
.FirstOrDefault();
string jsonPath = att != null ? att.PropertyName : prop.Name;
if (serializer.ContractResolver is DefaultContractResolver)
{
var resolver = (DefaultContractResolver)serializer.ContractResolver;
jsonPath = resolver.GetResolvedPropertyName(jsonPath);
}
var nesting = jsonPath.Split('.');
JObject lastLevel = main;
for (int i = 0; i < nesting.Length; i++)
{
if (i == nesting.Length - 1)
{
lastLevel[nesting[i]] = new JValue(prop.GetValue(value));
}
else
{
if (lastLevel[nesting[i]] == null)
{
lastLevel[nesting[i]] = new JObject();
}
lastLevel = (JObject)lastLevel[nesting[i]];
}
}
}
serializer.Serialize(writer, main);
}
}
en lugar de hacer
lastLevel [nesting [i]] = new JValue(prop.GetValue (value));
Tu tienes que hacer
lastLevel[nesting[i]] = JValue.FromObject(jValue);
De lo contrario tenemos un
No se pudo determinar el tipo de objeto JSON para el tipo...
excepción
Un fragmento de código completo sería este:
object jValue = prop.GetValue(value);
if (prop.PropertyType.IsArray)
{
if(jValue != null)
//https://stackoverflow.com/a/20769644/249895
lastLevel[nesting[i]] = JArray.FromObject(jValue);
}
else
{
if (prop.PropertyType.IsClass && prop.PropertyType != typeof(System.String))
{
if (jValue != null)
lastLevel[nesting[i]] = JValue.FromObject(jValue);
}
else
{
lastLevel[nesting[i]] = new JValue(jValue);
}
}