중첩된 JsonObject 역직렬화 방법이 궁금합니다.

{
    "type": "example",
    "token": "example",
    "action_ts": "example",
    "team": {
        "id": "example",
        "domain": "example"
    },
    "user": {
        "id": "example",
        "username": "example",
        "team_id": "example",
        "name": "example"
    },
     //  
     "blocks": [
            {
                "type": "example",
                "block_id": "example/",
                "text": {
                    "type": "example",
                    "text": "example",
                    "verbatim": false
                }
            },
           // 원하는 정보는 여기 있습니다.
            {
                "type": "targetType",
                "block_id": "example",
                "text": {
                    "type": "example",
                    "text": "targetText",
                    "verbatim": false
                }
            },
public class DeserializeModel
{
   ...
   public JsonArray Blocks { get; set;}
   ...
}
// 코드 품질 이슈....
public GetTextOfTarget(JsonArray DeserializeModel.Blocks)
{
   	foreach (var block in blocks)
	{
		var items = block.AsObject();

        ...
        ...
        ...

		if (isTarget: true)
		{
			foreach (var item in items)
			{
				if (item.Key.Equals("text"))
				{
					foreach (var textItem in item.Value.AsObject())
					{
						if (textItem.Key.Equals("text"))
						{
							text = textItem.Value.ToString();
							break;
						}
					}

					break;
				}
			}
		}

		break;
	}
}

중첩된 JosnObject에서 원하는 데이터를 찾기 위해 코드를 작성했습니다.
GetTextOfTarget가 if와 foreach문이 중첩의 중첩이 되서 가독성의 문제 등…이 있지 않나 생각이 듭니다.

역직렬화 Model을 작성하여 key에 매칭 시키는 방법도 있긴한데(DeserializeModel처럼), 여러 종류의 Json 데이터 값이 들어오고 그 것에 맞는 Model을 일일이 작성하는 것도 아닌거 같구요,
결론은 코드 품질을 높이고 싶은데, 좋은 방법을 모르겠습니다



분명히 누군가는 저와 같은 생각을 하고 같은 상황을 걲었을 텐데, 좋은 예제가 안 보이는군요.
보통 이런 경우 검색 키워드 선정이 잘 못 되었거나, 간단한건데 삽질하는 경우 더라구요.

3개의 좋아요

깊이 공감합니다.

여러 종류의 Json 형태가 들어오는데, 거기에 맞게 Model 클래스를 다 작성하기엔 일이 너무 많을 때
저는 dynamic으로 처리를 했던 것 같습니다… (기억이 안 나네요;)

일단 구글링 해보니 마제에 참고할 만한게 올라와있긴 합니다.

Newton.Json을 쓰시는거라면

여기를 참고하셔서 아래와 같이 한다면, 목표로 하는데에 더 가까울 것 같아요.

var definition = new { Name = "" };

string json1 = @"{'Name':'James'}";
var customer1 = JsonConvert.DeserializeAnonymousType(json1, definition);

Console.WriteLine(customer1.Name);
// James

string json2 = @"{'Name':'Mike'}";
var customer2 = JsonConvert.DeserializeAnonymousType(json2, definition);

Console.WriteLine(customer2.Name);
// Mike

원하시는 내용이었을지 모르겠습니다…^^;;

검색해서 찾아본다면 Dynamic이나 Anonymous가 키워드로 좋겠네요

4개의 좋아요

dynamic 으로 역직렬화 시켜서 객체 처럼 다루면 좀 더 직관적으로
해당 속성을 찾아 접근하실 수 있습니다.

다만 newtonsoft 의 Newton.Json 라이브러리 같은걸 사용하지 않는 다면
Dynamic 으로 역직렬화된 결과에 배열이 있는 경우 기본적으로 반복문 등으로 바로 탐색이 불가능 합니다.

[불가능 코드]

dynamic jsonObj = JsonSerializer.Deserialize<ExpandoObject>(json);
// 이렇게 바로 반복 탐색 불가능
foreach(var item jsonObj.ArrayObj) {
}

때문에 System.Text.Json 를 사용하신다면 해당 타입에 맞게 형식을 맞추어 반환해 주어야 하는데
다음은 System.Text.Json 를 사용한 예제 입니다.

string jsonString = File.ReadAllText("json.txt");  // 올려주신 JSON 예제 파일 그대로 복사 해서 파일로 저장했습니다.
JsonDocument doc = JsonDocument.Parse(jsonString);

JsonElement root = doc.RootElement;
dynamic dynamicObject = this.GetDynamicObjectFromJsonElement(root);

// blocks key가 존재 하는지 체크
if (((IDictionary<string, object>)dynamicObject).ContainsKey("blocks"))
{
                foreach(var block in dynamicObject.blocks)
                {
                    if(block.type == "targetType")
                    {
                        // 찾았다!
                        Console.WriteLine(block.type);
                        Console.WriteLine(block.block_id);
                        Console.WriteLine(block.text.text);
                        Console.WriteLine(block.text.verbatim);
                    }
                }
}

/// <summary>
/// JsonElement을 Dynamic 익명 객체로 변환 합니다.
/// </summary>
/// <param name="element"></param>
/// <returns></returns>
/// <exception cref="InvalidOperationException"></exception>
private dynamic GetDynamicObjectFromJsonElement(JsonElement element)
{
            switch (element.ValueKind)
            {
                case JsonValueKind.Object:
                    IDictionary<string, object> dict = new ExpandoObject();
                    foreach (JsonProperty property in element.EnumerateObject())
                    {
                        dict[property.Name] = GetDynamicObjectFromJsonElement(property.Value);
                    }
                    return dict;

                case JsonValueKind.Array:
                    List<object> list = new List<object>();
                    foreach (JsonElement arrayElement in element.EnumerateArray())
                    {
                        list.Add(GetDynamicObjectFromJsonElement(arrayElement));
                    }
                    return list;

                case JsonValueKind.String:
                    return element.GetString();

                case JsonValueKind.Number:
                    // 정수와 실수 구분하지 않고 double로 반환
                    return element.GetDouble();

                case JsonValueKind.True:
                    return true;

                case JsonValueKind.False:
                    return false;

                case JsonValueKind.Null:
                    return null;

                default:
                    throw new InvalidOperationException($"Unknown element value kind '{element.ValueKind}'.");
            }
}
5개의 좋아요

nested json?
2…3중첩 루프 정도는 괜찮지 않나요?
전 일부러 중첩루프 쓸 거 같은데… 정규식도 느려서 잘 안 쓰는 스타일이라…

2개의 좋아요

이 코드 제 도구함에 넣어두고, 마치 제가 작성한 척 음흉한 의도로 사사로이 써도 될까요? ^^

4개의 좋아요

도구함 공유해 주세요 ^^:grinning:

3개의 좋아요

@aroooong 질문 하나 더 해도 될까요?

JsonArray jsonArray = 
{
    "blocks": [
            {
                "type": "example",
                "block_id": "example/",
                "text": {
                    "type": "example",
                    "text": "example",
                    "verbatim": false
                }
            },
           // 원하는 정보는 여기 있습니다.
            {
                "type": "targetType",
                "block_id": "example",
                "text": {
                    "type": "example",
                    "text": "targetText",
                    "verbatim": false
                }
            }]
}

dynamic 개체를 만드는게 핵심으로 보이는데, jsonArray (blocks) 타입을 dynamic 개체로 만들 수 있나요?

1개의 좋아요

이렇게 멋진 코드를 제일 먼저 넣으려고 비워뒀었죠. ^^

이참에 누겟 패키지 하나 만들어보세요.

프레임워크라고 하기에는 뭐하지만, 급한 사람들 요긴하게 쓸 수 있는 코드 도구 모아 놓는 것도 괜찮을 것 같습니다.

2개의 좋아요

그냥… JsonNode를 이용하면 되지 않을까요?

var json = """
    ...
    """;

var jsonNode = JsonNode.Parse(json)!;
var blocks = jsonNode["blocks"]!;
foreach (var block in blocks.AsArray())
{
    if (block is null)
        continue;

    if (block["type"]!.GetValue<string>() is "targetType")
    {
        Console.WriteLine(block["text"]!["text"]!.GetValue<string>());
    }
}

| 출력

targetText
3개의 좋아요

코딩 너무 어렵습니다 ㅋㅋㅋㅋ
WebApi 관련해서 기본기가 많이 부족하네요

1개의 좋아요

Web API에서 필요한 기술을 먼저 목록으로 만들고

각 기술의 최소 사용법을 먼저 숙달한 다음

그것을 강화하는 방법은 어떨까요?

질문의 목적을 살펴보면 질문은 JsonObject의 중첩된 형태를 어떻게 해야 간단히 역직렬화 하는가 인데요, 제가 오늘 다시 질문을 살펴보니 어떻게 해야 중첩된 다양한 JSON 형태를 쉽게 사용할 수 있는가? 인 것 같습니다.

C#에서 다양한 방식으로 가능하지만 Web API를 사용한다면 이미 Swagger에 의해 형식을 클래스로 자동으로 변환할 수 있을텐데요, 관련된 내용을 살펴보시고,

JSON을 데이터 또는 스키마를 이용해서 클래스로 변환해주는 도구가 이미 있습니다.

예를 들어 Convert JSON to C# Classes Online - Json2CSharp Toolkit 이것을 이용했을 때 다음의 클래스 구조를 생성할 수 있습니다.

// Root myDeserializedClass = JsonConvert.DeserializeObject<Root>(myJsonResponse);
    public class Block
    {
        public string type { get; set; }
        public string block_id { get; set; }
        public Text text { get; set; }
    }

    public class Root
    {
        public string type { get; set; }
        public string token { get; set; }
        public string action_ts { get; set; }
        public Team team { get; set; }
        public User user { get; set; }
        public List<Block> blocks { get; set; }
    }

    public class Team
    {
        public string id { get; set; }
        public string domain { get; set; }
    }

    public class Text
    {
        public string type { get; set; }
        public string text { get; set; }
        public bool verbatim { get; set; }
    }

    public class User
    {
        public string id { get; set; }
        public string username { get; set; }
        public string team_id { get; set; }
        public string name { get; set; }
    }

기본기가 부족하다고 하지 마시고 어떻게 장악해나갈 것이냐 글로 기록하면서 구체화 하면 도움이 많이 됩니다.

먼저 일을 잘하는 방향과 개발을 잘하는 방향은 다르다는 것을 구분해야 하고요,

뭐든지 결과는 단계적으로 완전해지는 프로토타입의 형태일 수 밖에 없다는 한계도 정확히 이해해야 하고

결과의 목표 설정을 명확히 하시면 좋은 결과를 얻을 수 있습니다.

4개의 좋아요

좋은 예시가 하나 있어서 데리고 옵니다.

닷넷데브 포럼에서 화면의 목록 아래로 마우스 스크롤을 하면 이어서 다음 글 목록이 붙는 것을 확인할 수 있는데요, 그 목록은 다음의 질의로 가져옵니다.

https://forum.dotnetdev.kr/latest.json?no_definitions=true&page=1

이 JSON 데이터를 이용해서 위의 온라인 변환 툴을 이용해 다음의 클래스를 얻을 수 있습니다.

// Root myDeserializedClass = JsonConvert.DeserializeObject<Root>(myJsonResponse);
    public class Poster
    {
        public string extras { get; set; }
        public string description { get; set; }
        public int user_id { get; set; }
        public object primary_group_id { get; set; }
        public object flair_group_id { get; set; }
    }

    public class Root
    {
        public List<User> users { get; set; }
        public List<object> primary_groups { get; set; }
        public List<object> flair_groups { get; set; }
        public TopicList topic_list { get; set; }
    }

    public class TagsDescriptions
    {
    }

    public class Topic
    {
        public int id { get; set; }
        public string title { get; set; }
        public string fancy_title { get; set; }
        public string slug { get; set; }
        public int posts_count { get; set; }
        public int reply_count { get; set; }
        public int highest_post_number { get; set; }
        public string image_url { get; set; }
        public DateTime created_at { get; set; }
        public DateTime last_posted_at { get; set; }
        public bool bumped { get; set; }
        public DateTime bumped_at { get; set; }
        public string archetype { get; set; }
        public bool unseen { get; set; }
        public int last_read_post_number { get; set; }
        public int unread { get; set; }
        public int new_posts { get; set; }
        public int unread_posts { get; set; }
        public bool pinned { get; set; }
        public object unpinned { get; set; }
        public bool visible { get; set; }
        public bool closed { get; set; }
        public bool archived { get; set; }
        public int notification_level { get; set; }
        public bool bookmarked { get; set; }
        public bool liked { get; set; }
        public TagsDescriptions tags_descriptions { get; set; }
        public int views { get; set; }
        public int like_count { get; set; }
        public bool has_summary { get; set; }
        public string last_poster_username { get; set; }
        public int category_id { get; set; }
        public bool pinned_globally { get; set; }
        public object featured_link { get; set; }
        public bool has_accepted_answer { get; set; }
        public bool can_have_answer { get; set; }
        public List<Poster> posters { get; set; }
    }

    public class TopicList
    {
        public bool can_create_topic { get; set; }
        public string more_topics_url { get; set; }
        public int per_page { get; set; }
        public List<Topic> topics { get; set; }
    }

    public class User
    {
        public int id { get; set; }
        public string username { get; set; }
        public string name { get; set; }
        public string avatar_template { get; set; }
        public object flair_name { get; set; }
        public bool admin { get; set; }
        public bool moderator { get; set; }
        public int trust_level { get; set; }
    }

이제 이 클래스를 이용해서 특정 정보를 취해보겠습니다.

using System.Text.Json;

var c = new HttpClient();
var json = await c.GetStringAsync("https://forum.dotnetdev.kr/latest.json?no_definitions=true&page=1");

var page = JsonSerializer.Deserialize<Root>(json);
var topic = page!.topic_list.topics[0];
Console.WriteLine(topic.title);
Console.WriteLine(topic.views);
Console.WriteLine(page.users.FirstOrDefault(x => x.id == topic.posters[0].user_id)!.username);

// -----

// Root myDeserializedClass = JsonConvert.DeserializeObject<Root>(myJsonResponse);
public class Poster
{
    public string extras { get; set; }
    public string description { get; set; }
    public int user_id { get; set; }
    public object primary_group_id { get; set; }
    public object flair_group_id { get; set; }
}

public class Root
{
    public List<User> users { get; set; }
    public List<object> primary_groups { get; set; }
    public List<object> flair_groups { get; set; }
    public TopicList topic_list { get; set; }
}

public class TagsDescriptions
{
}

public class Topic
{
    public int id { get; set; }
    public string title { get; set; }
    public string fancy_title { get; set; }
    public string slug { get; set; }
    public int posts_count { get; set; }
    public int reply_count { get; set; }
    public int highest_post_number { get; set; }
    public string image_url { get; set; }
    public DateTime created_at { get; set; }
    public DateTime last_posted_at { get; set; }
    public bool bumped { get; set; }
    public DateTime bumped_at { get; set; }
    public string archetype { get; set; }
    public bool unseen { get; set; }
    public int last_read_post_number { get; set; }
    public int unread { get; set; }
    public int new_posts { get; set; }
    public int unread_posts { get; set; }
    public bool pinned { get; set; }
    public object unpinned { get; set; }
    public bool visible { get; set; }
    public bool closed { get; set; }
    public bool archived { get; set; }
    public int notification_level { get; set; }
    public bool? bookmarked { get; set; }
    public bool? liked { get; set; }
    public TagsDescriptions tags_descriptions { get; set; }
    public int views { get; set; }
    public int like_count { get; set; }
    public bool has_summary { get; set; }
    public string last_poster_username { get; set; }
    public int category_id { get; set; }
    public bool pinned_globally { get; set; }
    public object featured_link { get; set; }
    public bool has_accepted_answer { get; set; }
    public bool can_have_answer { get; set; }
    public List<Poster> posters { get; set; }
}

public class TopicList
{
    public bool can_create_topic { get; set; }
    public string more_topics_url { get; set; }
    public int per_page { get; set; }
    public List<Topic> topics { get; set; }
}

public class User
{
    public int id { get; set; }
    public string username { get; set; }
    public string name { get; set; }
    public string avatar_template { get; set; }
    public object flair_name { get; set; }
    public bool admin { get; set; }
    public bool moderator { get; set; }
    public int trust_level { get; set; }
}

| 결과

무엇인가 한다는 것
117
dimohy
3개의 좋아요

JsonPath 가 있습니다.
아래와 같은 링크를 참조 하시면 됩니다.

4개의 좋아요

이미 제가 보여드린 예시가
dynamic 으로 처리 된 것 입니다.

Dictionary 의 목록(list) 을 dynamic 으로요

2개의 좋아요

직렬화는 웹API에서 많이 쓰이기는 하지만, 웹API에 한정된다고 말할 수는 없는, 별도의 주제인 것 같습니다.

프로그래밍에서 사용되는 모든 주제를 전부 기억할 수는 없죠.
배웠다고 하더라도 사용할 일 없으면 까먹고, 사용할 일이 있으면 다시 리프레시하고… 그렇게 하는 수밖에 없죠.

저는 switch 문을 워낙 사용하지 않는 지라, C# 4년 차인 지금도 자동 완성 기능이 없으면 오락가락합니다.ㅠㅠ

3개의 좋아요