web-dev-qa-db-ja.com

クラスのプロパティをJSONの子プロパティにマップするために、属性にパスを指定できますか?

Newtonsoft.JsonのDeserializeObject<T>(strJSONData)を使用してWeb要求からデータを取得し、それをクラスオブジェクトに変換するコード(変更できません)があります(クラスを変更できます)。クラスプロパティを[DataMember(Name = "raw_property_name")]で装飾することにより、生のJSONデータをクラスの正しいプロパティにマッピングできます。 JSON複雑なオブジェクトの子プロパティを単純なプロパティにマップする方法はありますか?以下に例を示します。

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

URL以外の画像オブジェクトの残りは気にしないので、C#クラスに複雑なオブジェクトを設定したくありません。私は本当に次のようなものが欲しいだけです:

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

これは可能ですか?

40
David P

単一の追加プロパティが必要な場合は、JSONをJObjectに解析し、ToObject()を使用してJObjectからクラスを作成します。次に、SelectToken()を使用して追加のプロパティを取得します。

したがって、クラスが次のようになっていると仮定します。

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

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

    public string ProfilePicture { get; set; }
}

これを行うことができます:

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

フィドル: https://dotnetfiddle.net/7gnJCK


より洗練されたソリューションを好む場合は、カスタムJsonConverterを作成して、JsonProperty属性を説明どおりに動作させることができます。コンバーターはクラスレベルで動作し、上記の手法と組み合わせていくつかのリフレクションを使用して、すべてのプロパティを設定する必要があります。コードでは次のようになります。

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

デモンストレーションのために、JSONが次のようになったと仮定します。

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

...そして、以前の情報に加えて、その人の好きな映画(タイトルと年)と好きな色に興味があります。最初にターゲットクラスを[JsonConverter]属性でマークしてカスタムコンバーターに関連付け、次に各プロパティで[JsonProperty]属性を使用して、名前として目的のプロパティパス(大文字と小文字を区別する)を指定します。ターゲットプロパティもプリミティブである必要はありません。ここでMovieで行ったように、子クラスを使用できます(介在するFavoritesクラスが必要ないことに注意してください)。

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

すべての属性を設定したら、通常どおりデシリアライズすることができ、「機能する」はずです。

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

フィドル: https://dotnetfiddle.net/Ljw32O

51
Brian Rogers

マークされた回答は、CamelCasePropertyNamesContractResolverなどの登録される可能性のあるIContractResolverを無視するため、100%完全ではありません。

また、変換可能に対してfalseを返すと、他のユーザーケースが防止されるため、return objectType.GetCustomAttributes(true).OfType<JsonPathConverter>().Any();

更新されたバージョンは次のとおりです。 https://dotnetfiddle.net/F8C8U8

リンクに示されているように、プロパティにJsonPropertyを設定する必要もなくなりました。

何らかの理由で上記のリンクが停止または爆発した場合、以下のコードも含まれます:

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

する代わりに

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

あなたはしなければならない

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

それ以外の場合は

タイプのJSONオブジェクトタイプを特定できませんでした...

例外

完全なコードは次のようになります。

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

誰かが@BrianRogersのJsonPathConverterをWriteJsonオプションとともに使用する必要がある場合、解決策があります(ドットのみのパスでのみ機能します)。

CanWriteプロパティを削除して、デフォルトで再びtrueになるようにします。

WriteJsonコードを次のように置き換えます。

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

上で言ったように、これはdotsを含むパスに対してのみ機能します。その場合、他のケースを防ぐために、次のコードをReadJsonに追加する必要があります。

[...]
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