web-dev-qa-db-fra.com

Puis-je spécifier un chemin dans un attribut pour mapper une propriété de ma classe à une propriété enfant dans mon JSON?

Il y a du code (que je ne peux pas changer) qui utilise DeserializeObject<T>(strJSONData) de Newtonsoft.Json pour prendre les données d'une requête Web et les convertir en un objet de classe (je peux changer la classe). En décorant mes propriétés de classe avec [DataMember(Name = "raw_property_name")] je peux mapper les données JSON brutes à la propriété correcte dans ma classe. Existe-t-il un moyen de mapper la propriété enfant d'un objet complexe JSON à une propriété simple? Voici un exemple:

{
    "picture": 
    {
        "id": 123456,
        "data": 
        {
            "type": "jpg",
            "url": "http://www.someplace.com/mypicture.jpg"
        }
    }
}

Je ne me soucie pas du reste de l'objet d'image, sauf de l'URL, et je ne veux donc pas configurer un objet complexe dans ma classe C #. Je veux vraiment quelque chose comme:

[DataMember(Name = "picture.data.url")]
public string ProfilePicture { get; set; }

Est-ce possible?

40
David P

Eh bien, si vous avez juste besoin d'une seule propriété supplémentaire, une approche simple consiste à analyser votre JSON en JObject, utilisez ToObject() pour remplir votre classe à partir de JObject, et puis utilisez SelectToken() pour extraire la propriété supplémentaire.

Donc, en supposant que votre classe ressemblait à ceci:

class Person
{
    [JsonProperty("name")]
    public string Name { get; set; }

    [JsonProperty("age")]
    public string Age { get; set; }

    public string ProfilePicture { get; set; }
}

Vous pouvez faire ceci:

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");

Violon: https://dotnetfiddle.net/7gnJCK


Si vous préférez une solution plus sophistiquée, vous pouvez créer un JsonConverter personnalisé pour permettre à l'attribut JsonProperty de se comporter comme vous le décrivez. Le convertisseur devrait fonctionner au niveau de la classe et utiliser une réflexion combinée avec la technique ci-dessus pour remplir toutes les propriétés. Voici à quoi cela pourrait ressembler dans le code:

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();
    }
}

Pour démontrer, supposons que le JSON ressemble maintenant à ceci:

{
  "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"
  }
}

... et vous êtes intéressé par le film préféré de la personne (titre et année) et la couleur préférée en plus des informations d'avant. Vous devez d'abord marquer votre classe cible avec un [JsonConverter] attribut pour l'associer au convertisseur personnalisé, puis utilisez [JsonProperty] attributs sur chaque propriété, spécifiant le chemin de propriété souhaité (sensible à la casse) comme nom Les propriétés cibles ne doivent pas non plus être des primitives - vous pouvez utiliser une classe enfant comme je l'ai fait ici avec Movie (et notez qu'aucune classe Favorites intermédiaire n'est requise).

[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; }
}

Avec tous les attributs en place, vous pouvez simplement désérialiser comme d'habitude et cela devrait "simplement fonctionner":

Person p = JsonConvert.DeserializeObject<Person>(json);

Violon: https://dotnetfiddle.net/Ljw32O

51
Brian Rogers

La réponse marquée n'est pas complète à 100% car elle ignore tout IContractResolver qui peut être enregistré tel que CamelCasePropertyNamesContractResolver etc.

Renvoyer également false pour peut convertir empêchera d'autres cas d'utilisation, donc je l'ai changé en return objectType.GetCustomAttributes(true).OfType<JsonPathConverter>().Any();

Voici la version mise à jour: https://dotnetfiddle.net/F8C8U8

J'ai également supprimé la nécessité de définir un JsonProperty sur une propriété comme illustré dans le lien.

Si pour une raison quelconque, le lien ci-dessus meurt ou explose, y compris le code ci-dessous:

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);
        }
    }
12
Robert Ghafoor

Au lieu de faire

lastLevel [nesting [i]] = new JValue(prop.GetValue (value));

Tu dois faire

lastLevel[nesting[i]] = JValue.FromObject(jValue);

Sinon, nous avons un

Exception.

un morceau de code complet serait le suivant:

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); } }

8
Pierre-Yves Dufour

Si quelqu'un doit également utiliser le JsonPathConverter de @BrianRogers avec l'option WriteJson, voici une solution (qui ne fonctionne que pour les chemins avec points uniquement):

Supprimez la propriété CanWrite pour qu'elle redevienne par défaut true.

Remplacez le code WriteJson par ce qui suit:

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);
        var nesting=jsonPath.Split(new[] { '.' });
        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);
}

Comme je l'ai dit ci-dessus, cela ne fonctionne que pour les chemins qui contiennent points. Étant donné que, vous devez ajouter le code suivant à ReadJson afin d'éviter d'autres cas:

[...]
string jsonPath = (att != null ? att.PropertyName : prop.Name);
if (!Regex.IsMatch(jsonPath, @"^[a-zA-Z0-9_.-]+$")) {
    throw new InvalidOperationException("JProperties of JsonPathConverter can have only letters, numbers, underscores, hiffens and dots."); //Array operations not permitted
}
JToken token = jo.SelectToken(jsonPath);
[...]
5