0

I have a json with different kind of blocks in it. There are simple block, like text, there's no problems in deserializing them, so I won't cover them up. The problems are with three kind of blocks: media, list and quiz:

{
  "blocks": [
    {
      "type": "text",
      "data": {
           "text": "my awesome text",
           "text_truncated": "<<<same>>>"
        },
      "cover": false,
      "anchor": ""
    },
    {
      "type": "media",
      "data": {
        "items": [
          {
            "title": "title1",
            "author": "author1",
            "image": {
              "type": "image",
              "data": {
                "uuid": "eb19f678-3c9f-58f0-90c2-33bcb8237b17",
                "width": 1024,
                "height": 756,
                "size": 448952,
                "type": "jpg",
                "color": "d3c58f",
                "hash": "",
                "external_service": []
              }
            }
          },
          {
            "title": "title2",
            "author": "author2",
            "image": {
              "type": "image",
              "data": {
                "uuid": "9274038e-1e9b-5cab-9db5-4936ce88a5c9",
                "width": 750,
                "height": 563,
                "size": 164261,
                "type": "jpg",
                "color": "b7a58d",
                "hash": "",
                "external_service": []
              }
            }
          }
        ],
        "with_background": false,
        "with_border": false
      },
      "cover": false,
      "anchor": ""
    },
    {
      "type": "list",
      "data": {
        "items": [
          "foo",
          "bar"
        ],
        "type": "UL"
      },
      "cover": false,
      "anchor": ""
    },
    {
      "type": "quiz",
      "data": {
        "uid": "00bde249ff735f481620328765695",
        "hash": "29d6bf8fec36eee3",
        "tmp_hash": "",
        "title": "When?",
        "items": {
          "a16203287650": "Ashita",
          "a16203287651": "Kinou",
          "a16203287742": "Ima"
        },
        "is_public": false,
        "date_created": 1620328765
      },
      "cover": false,
      "anchor": ""
    }
  ]
}

These blocks's items property is different from one to another.

For serializing/deserializing I'm using System.Text.Json, built in net5.0. While I can deserialize this json's items into JsonElement and work with it further, I'd like to deserialize them to exact classes automatically, when I call JsonSerializer.Deserialize(...); e.g. IEnumerable<MediaBlockData>, IEnumerable<string> and so on, depending on what type (I mean json's property) it is.

While looking for answer I found out an article on mircosoft website, but I'm not wholly understand on how to implement it for my case:

https://learn.microsoft.com/en-us/dotnet/standard/serialization/system-text-json-converters-how-to?pivots=dotnet-5-0

Update 1

My blocks implementation:

    public class Block
    {
        [JsonPropertyName("type")]
        public string Type { get; set; }

        [JsonPropertyName("data")]
        public JsonElement Data { get; set; }

        public IBlockData ParsedData { get; set; }

        [JsonPropertyName("cover")]
        public bool Cover { get; set; }

        [JsonPropertyName("anchor")]
        public string Anchor { get; set; }

        private Type GetBlockDataType() => Type switch
        {
            "audio" => typeof(AudioBlockData),
            "code" => typeof(CodeBlockData),
            "delimiter" => typeof(DelimiterBlockData),
            "header" => typeof(HeaderBlockData),
            "image" => typeof(ImageBlockData),
            "incut" => typeof(IncutBlockData),
            "instagram" => typeof(InstagramBlockData),
            "link" => typeof(LinkBlockData),
            "list" => typeof(ListBlockData),
            "media" => typeof(MediaBlockData),
            "number" => typeof(NumberBlockData),
            "person" => typeof(PersonBlockData),
            "quiz" => typeof(QuizBlockData),
            "quote" => typeof(QuoteBlockData),
            "spotify" => typeof(SpotifyBlockData),
            "telegram" => typeof(TelegramBlockData),
            "text" => typeof(TextBlockData),
            "tiktok" => typeof(TikTokBlockData),
            "tweet" => typeof(TweetBlockData),
            "universalbox" => typeof(UniversalBoxBlockData),
            "video" => typeof(VideoBlockData),
            "yamusic" => typeof(YaMusicBlockData),
            _ => typeof(object)
        };

        public IBlockData GetBlockData()
        {
            var blockType = GetBlockDataType();

            return (IBlockData)JsonSerializer.Deserialize(Data.ToString(), blockType);
        }
    }
    
    public class ListBlockData : IBlockData
    {
        [JsonPropertyName("items")]
        public IEnumerable<string> Items { get; set; }

        [JsonPropertyName("type")]
        public string Type { get; set; }
    }
    
    public class QuizBlockData : IBlockData
    {
        [JsonPropertyName("uid")]
        public string Uid { get; set; }

        [JsonPropertyName("hash")]
        public string Hash { get; set; }

        [JsonPropertyName("tmp_hash")]
        public string TempHash { get; set; }

        [JsonPropertyName("title")]
        public string Title { get; set; }

        [JsonPropertyName("items")]
        public JsonElement Items { get; set; }

        [JsonPropertyName("is_public")]
        public bool IsPublic { get; set; }

        [JsonPropertyName("date_created")]
        public long DateCreated { get; set; }
    }
    
    public class MediaBlockData : IBlockData
    {
        [JsonPropertyName("items")]
        public IEnumerable<MediaItem> Items { get; set; }

        [JsonPropertyName("with_background")]
        public bool WithBackground { get; set; }

        [JsonPropertyName("with_border")]
        public bool WithBorder { get; set; }
    }

    public class MediaItem : Block
    {
        [JsonPropertyName("title")]
        public string Title { get; set; }

        [JsonPropertyName("author")]
        public string Author { get; set; }

        [JsonPropertyName("image")]
        public Block Image { get; set; }
    }

Update 2

Here's minimal reproducable code sample with json string in it:

using System;
using System.Collections.Generic;
using System.Text.Json;
using System.Text.Json.Serialization;

namespace ConsoleApp1
{
    public interface IBlockData
    {

    }

    public class TextBlockData : IBlockData
    {
        [JsonPropertyName("text")]
        public string Text { get; set; }

        [JsonPropertyName("text_truncated")]
        public string TextTruncated { get; set; }
    }

    public class MediaItem : Block
    {
        [JsonPropertyName("title")]
        public string Title { get; set; }

        [JsonPropertyName("author")]
        public string Author { get; set; }

        [JsonPropertyName("image")]
        public Block Image { get; set; }
    }

    public class QuizBlockData : IBlockData
    {
        [JsonPropertyName("uid")]
        public string Uid { get; set; }

        [JsonPropertyName("hash")]
        public string Hash { get; set; }

        [JsonPropertyName("tmp_hash")]
        public string TempHash { get; set; }

        [JsonPropertyName("title")]
        public string Title { get; set; }

        [JsonPropertyName("items")]
        public JsonElement Items { get; set; } // TODO: normal class instead of JsonElement

        [JsonPropertyName("is_public")]
        public bool IsPublic { get; set; }

        [JsonPropertyName("date_created")]
        public long DateCreated { get; set; }
    }

    public class MediaBlockData : IBlockData
    {
        [JsonPropertyName("items")]
        public IEnumerable<MediaItem> Items { get; set; }

        [JsonPropertyName("with_background")]
        public bool WithBackground { get; set; }

        [JsonPropertyName("with_border")]
        public bool WithBorder { get; set; }
    }

    public class ListBlockData : IBlockData
    {
        [JsonPropertyName("items")]
        public IEnumerable<string> Items { get; set; }

        [JsonPropertyName("type")]
        public string Type { get; set; }
    }

    public class ImageBlockData : IBlockData
    {
        [JsonPropertyName("uuid")]
        public Guid Uuid { get; set; }

        [JsonPropertyName("width")]
        public int Width { get; set; }

        [JsonPropertyName("height")]
        public int Height { get; set; }

        [JsonPropertyName("size")]
        public long Size { get; set; }

        [JsonPropertyName("type")]
        public string Type { get; set; }

        [JsonPropertyName("color")]
        public string Color { get; set; }

        [JsonPropertyName("hash")]
        public string Hash { get; set; }

        [JsonPropertyName("external_service")]
        public IEnumerable<ExternalService> ExternalService { get; set; }
    }

    public class ExternalService
    {
        [JsonPropertyName("name")]
        public string Name { get; set; }

        [JsonPropertyName("id")]
        public string Id { get; set; }
    }

    public class Block
    {
        [JsonPropertyName("type")]
        public string Type { get; set; }

        [JsonPropertyName("data")]
        public JsonElement Data { get; set; }

        public IBlockData ParsedData { get; set; }

        [JsonPropertyName("cover")]
        public bool Cover { get; set; }

        [JsonPropertyName("anchor")]
        public string Anchor { get; set; }

        private Type GetBlockDataType() => Type switch
        {
            "image" => typeof(ImageBlockData),
            "list" => typeof(ListBlockData),
            "media" => typeof(MediaBlockData),
            "quiz" => typeof(QuizBlockData),
            "text" => typeof(TextBlockData),
            _ => typeof(object)
        };

        public IBlockData GetBlockData()
        {
            var blockType = GetBlockDataType();

            return (IBlockData)JsonSerializer.Deserialize(Data.ToString(), blockType);
        }
    }

    public class Root
    {
        [JsonPropertyName("blocks")]
        public IEnumerable<Block> Blocks { get; set; }
    }

    internal static class Program
    {
        private static void Main()
        {
            var json = "{\"blocks\":[{\"type\":\"text\",\"data\":{\"text\":\"my awesome text\",\"text_truncated\":\"<<<same>>>\"},\"cover\":false,\"anchor\":\"\"},{\"type\":\"media\",\"data\":{\"items\":[{\"title\":\"title1\",\"author\":\"author1\",\"image\":{\"type\":\"image\",\"data\":{\"uuid\":\"eb19f678-3c9f-58f0-90c2-33bcb8237b17\",\"width\":1024,\"height\":756,\"size\":448952,\"type\":\"jpg\",\"color\":\"d3c58f\",\"hash\":\"\",\"external_service\":[]}}},{\"title\":\"title2\",\"author\":\"author2\",\"image\":{\"type\":\"image\",\"data\":{\"uuid\":\"9274038e-1e9b-5cab-9db5-4936ce88a5c9\",\"width\":750,\"height\":563,\"size\":164261,\"type\":\"jpg\",\"color\":\"b7a58d\",\"hash\":\"\",\"external_service\":[]}}}],\"with_background\":false,\"with_border\":false},\"cover\":false,\"anchor\":\"\"},{\"type\":\"list\",\"data\":{\"items\":[\"foo\",\"bar\"],\"type\":\"UL\"},\"cover\":false,\"anchor\":\"\"},{\"type\":\"quiz\",\"data\":{\"uid\":\"00bde249ff735f481620328765695\",\"hash\":\"29d6bf8fec36eee3\",\"tmp_hash\":\"\",\"title\":\"When?\",\"items\":{\"a16203287650\":\"Ashita\",\"a16203287651\":\"Kinou\",\"a16203287742\":\"Ima\"},\"is_public\":false,\"date_created\":1620328765},\"cover\":false,\"anchor\":\"\"}]}";

            var root = JsonSerializer.Deserialize<Root>(json);

            // Example on how it can be deserialized afterwards
            foreach (Block block in root.Blocks)
            {
                block.ParsedData = block.GetBlockData();
            }
        }
    }
}

Update 3

I've been able to deserialize blocks correctly (hope there are no major issues with this code), thanks to @dbc and this post:

Deserialize JSON array which has mixed values System.Text.JSON

Updated code:

using System;
using System.Collections.Generic;
using System.Text.Json;
using System.Text.Json.Serialization;

namespace ConsoleApp1
{
    public class BlockData
    {

    }

    public class TextBlockData : BlockData
    {
        [JsonPropertyName("text")]
        public string Text { get; set; }

        [JsonPropertyName("text_truncated")]
        public string TextTruncated { get; set; }
    }

    public class MediaItemBlock : Block
    {
        [JsonPropertyName("title")]
        public string Title { get; set; }

        [JsonPropertyName("author")]
        public string Author { get; set; }

        [JsonPropertyName("image")]
        public Block Image { get; set; }
    }

    public class QuizBlockData : BlockData
    {
        [JsonPropertyName("uid")]
        public string Uid { get; set; }

        [JsonPropertyName("hash")]
        public string Hash { get; set; }

        [JsonPropertyName("tmp_hash")]
        public string TempHash { get; set; }

        [JsonPropertyName("title")]
        public string Title { get; set; }

        [JsonPropertyName("items")]
        public Dictionary<string, string> Items { get; set; }

        [JsonPropertyName("is_public")]
        public bool IsPublic { get; set; }

        [JsonPropertyName("date_created")]
        public long DateCreated { get; set; }
    }

    public class MediaBlockData : BlockData
    {
        [JsonPropertyName("items")]
        public IEnumerable<MediaItemBlock> Items { get; set; }

        [JsonPropertyName("with_background")]
        public bool WithBackground { get; set; }

        [JsonPropertyName("with_border")]
        public bool WithBorder { get; set; }
    }

    public class ListBlockData : BlockData
    {
        [JsonPropertyName("items")]
        public IEnumerable<string> Items { get; set; }

        [JsonPropertyName("type")]
        public string Type { get; set; }
    }

    public class ImageBlockData : BlockData
    {
        [JsonPropertyName("uuid")]
        public Guid Uuid { get; set; }

        [JsonPropertyName("width")]
        public int Width { get; set; }

        [JsonPropertyName("height")]
        public int Height { get; set; }

        [JsonPropertyName("size")]
        public long Size { get; set; }

        [JsonPropertyName("type")]
        public string Type { get; set; }

        [JsonPropertyName("color")]
        public string Color { get; set; }

        [JsonPropertyName("hash")]
        public string Hash { get; set; }

        [JsonPropertyName("external_service")]
        public IEnumerable<ExternalService> ExternalService { get; set; }
    }

    public class ExternalService
    {
        [JsonPropertyName("name")]
        public string Name { get; set; }

        [JsonPropertyName("id")]
        public string Id { get; set; }
    }

    public class Block
    {
        [JsonPropertyName("type")]
        public string Type { get; set; }

        [JsonPropertyName("data")]
        public BlockData Data { get; set; }

        [JsonPropertyName("cover")]
        public bool Cover { get; set; }

        [JsonPropertyName("anchor")]
        public string Anchor { get; set; }

        public static Type GetBlockDataType(string type) => type switch
        {
            "image" => typeof(ImageBlockData),
            "list" => typeof(ListBlockData),
            "media" => typeof(MediaBlockData),
            "quiz" => typeof(QuizBlockData),
            "text" => typeof(TextBlockData),
            _ => typeof(object)
        };
    }

    public class Root
    {
        [JsonPropertyName("blocks")]
        public IEnumerable<Block> Blocks { get; set; }
    }

    internal static class Program
    {
        private static void Main()
        {
            var json = "{\"blocks\":[{\"type\":\"text\",\"data\":{\"text\":\"my awesome text\",\"text_truncated\":\"<<<same>>>\"},\"cover\":false,\"anchor\":\"\"},{\"type\":\"media\",\"data\":{\"items\":[{\"title\":\"title1\",\"author\":\"author1\",\"image\":{\"type\":\"image\",\"data\":{\"uuid\":\"eb19f678-3c9f-58f0-90c2-33bcb8237b17\",\"width\":1024,\"height\":756,\"size\":448952,\"type\":\"jpg\",\"color\":\"d3c58f\",\"hash\":\"\",\"external_service\":[]}}},{\"title\":\"title2\",\"author\":\"author2\",\"image\":{\"type\":\"image\",\"data\":{\"uuid\":\"9274038e-1e9b-5cab-9db5-4936ce88a5c9\",\"width\":750,\"height\":563,\"size\":164261,\"type\":\"jpg\",\"color\":\"b7a58d\",\"hash\":\"\",\"external_service\":[]}}}],\"with_background\":false,\"with_border\":false},\"cover\":false,\"anchor\":\"\"},{\"type\":\"list\",\"data\":{\"items\":[\"foo\",\"bar\"],\"type\":\"UL\"},\"cover\":false,\"anchor\":\"\"},{\"type\":\"quiz\",\"data\":{\"uid\":\"00bde249ff735f481620328765695\",\"hash\":\"29d6bf8fec36eee3\",\"tmp_hash\":\"\",\"title\":\"When?\",\"items\":{\"a16203287650\":\"Ashita\",\"a16203287651\":\"Kinou\",\"a16203287742\":\"Ima\"},\"is_public\":false,\"date_created\":1620328765},\"cover\":false,\"anchor\":\"\"}]}";

            var options = new JsonSerializerOptions
            {
                Converters = { new BlockConverter() }
            };

            var root = JsonSerializer.Deserialize<Root>(json, options);
            var ser = JsonSerializer.Serialize(root, options);
        }
    }

    public class BlockConverter : JsonConverter<Block>
    {
        public override Block Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
        {
            using JsonDocument doc = JsonDocument.ParseValue(ref reader);

            // Get type for convert
            string type = doc.RootElement.GetProperty("type").GetString();

            // Create new block with default deserializer
            Block block = JsonSerializer.Deserialize<Block>(doc.RootElement.GetRawText());

            // Warning: recursive for types with blocks inside
            block.Data = (BlockData)JsonSerializer.Deserialize(doc.RootElement.GetProperty("data").GetRawText(),
                                                               Block.GetBlockDataType(type), options);

            return block;
        }

        public override void Write(Utf8JsonWriter writer, Block value, JsonSerializerOptions options)
        {
            throw new NotImplementedException();
        }
    }
}

Now I'm trying to serialize it back to string correctly, not yet happening.

Gigas002
  • 13
  • 7
  • Can you also add the class structure and code you are using in the code? You shouldn't need a custom converter for this as object deserialisation is supported by the .Net5 library. – blockingHD May 07 '21 at 12:55
  • 1
    @blockingHD sure! I've updated the question with code snippet. – Gigas002 May 07 '21 at 13:06
  • Looks like you have a polymorphic type hierarchy. Serialization of such hierarchies is not supported out of the box by `System.Text.Json`. You will need to write a custom converter to do that. To do so, see [Is polymorphic deserialization possible in System.Text.Json?](https://stackoverflow.com/q/58074304/3744182). Does that answer your question, or do you need additional help? – dbc May 07 '21 at 13:16
  • @dbc thank you for your answer! I've seen similar example on microsoft guilde to json converters for `System.Text.Json`, but I honestly don't quite understand on how to implement it for my case. – Gigas002 May 07 '21 at 13:31
  • 1
    Then can you strip your code down into a [mcve]? The code you have shared so far does not even compile, see https://dotnetfiddle.net/8UT9r4. Without a complete, compilable example, all we can do is point you to [Is polymorphic deserialization possible in System.Text.Json?](https://stackoverflow.com/q/58074304/3744182) – dbc May 07 '21 at 13:40
  • @dbc I've updated the question with minimal reproducable code sample with json from this question included. – Gigas002 May 07 '21 at 14:07
  • @dbc I've managed to deserialize json correctly, thanks for your help! Now I'm stuck at serializing it back to string though – Gigas002 May 07 '21 at 19:36

1 Answers1

1

Here's a version that desieralises this json without using a converter.

var media = new List<MediaBlock>();
var texts = new List<TextBlock>();

using var doc = JsonDocument.Parse(json);
foreach (var block in doc.RootElement.GetProperty("blocks").EnumerateArray())
{
    switch (block.GetProperty("type").GetString())
    {
        case "text": texts.Add(Deserialise<TextBlock>(block.GetProperty("data"))); break;
        case "media": media.Add(Deserialise<MediaBlock>(block.GetProperty("data"))); break;
        // ... 
    }
}

var composite = new 
{
    Texts = texts,// compsite.Texts[0].Text is 'my awesome text'
    Media = media // compsite.Media.Items[0].Author is 'author1'
};

// This is OK, but if you need speed, please have a look at
// https://stackoverflow.com/questions/58138793/system-text-json-jsonelement-toobject-workaround
static T Deserialise<T>(JsonElement e) => JsonSerializer.Deserialize<T>(e.GetRawText(), options: new JsonSerializerOptions { PropertyNameCaseInsensitive = true });

The auxiliary classes I used are:

class MediaBlock
{
    public List<MediaItem> Items { get; set; }
}

class MediaItem
{
    public string Author { get; set; }
    public string Title { get; set; }
    // ...
}

class TextBlock
{
    public string Text { get; set; }
}
tymtam
  • 31,798
  • 8
  • 86
  • 126