skip to Main Content

I am trying to serialize and deserialize a class (say A) into Json.
The class has multiple properties (say X1, X2, X3) which are object instances (of class X) which contains large lists.

public class A
{
    public X X1 { get; set; }

    public X X2 { get; set; }

    public X X3 { get; set; }
}

public class X
{
    // This should request the serializer to put this into a separate file rather than within the parent's json
    [JsonIgnore]
    public bool SplitJson { get; set; }

    public List<Y> LargeList { get; set; }
}

public class Y
{
    public List<object> AnotherList { get; set; }
}

public class Temp
{
    public static void JsonTest()
    {
        A a = new()
        {
            X1 = new()
            {
                LargeList =
                [
                    new()
                    {
                        AnotherList = ["placeholder"]
                    }
                ],
                SplitJson = true
            },
            X2 = new()
            {
                LargeList =
                [
                    new()
                    {
                        AnotherList = ["dummy"]
                    }
                ],
                SplitJson = true
            },
            X3 = new()
            {
                LargeList =
                [
                    new()
                    {
                        AnotherList = ["text"]
                    }
                ]
            }
        };
        JsonSerializerOptions options = new() { WriteIndented = true };
        Debug.WriteLine(JsonSerializer.Serialize(a, options));
    }
}

While I can serialize those instances (of class X) individually and load them into my main class (A), I need a way to serialize class A into Json in such a way that its properties (X1 & X2) are serialized into relative file paths ("X1.json" & "X2.json") only when they request so.

For example: This is the normal output

{
  "X1": {
    "LargeList": [
      {
        "AnotherList": [
          "placeholder"
        ]
      }
    ]
  },
  "X2": {
    "LargeList": [
      {
        "AnotherList": [
          "dummy"
        ]
      }
    ]
  },
  "X3": {
    "LargeList": [
      {
        "AnotherList": [
          "text"
        ]
      }
    ]
  }
}

What I need is something like this:

{
  "X1": "X1.json",
  "X2": "X2.json",
  "X3": {
    "LargeList": [
      {
        "AnotherList": [
          "text"
        ]
      }
    ]
  }
}

I need to control the serialization by means of a Boolean property in class X called SplitJson. If enabled the serializer should serialize the instances of X with SplitJson set to true into relative paths. Then those instances of X of those properties should be serialized into the corresponding files. This should also work with deserialization.

Also, are there any ways to implement delayed (or async) setting of properties? I need the class A to load first but the properties with relative paths to json files should not be deserialized until required. But the relative paths to the separate Jsons should be stored somewhere and must deserialize when needed.

2

Answers


  1. Chosen as BEST ANSWER

    @sinatr, Thank you so much for your guidance!

    I was really confused with Json Serialization in System.Text.Json. I thought I would have to write my own converter to handle writing and reading all properties by myself and put into consideration of polymorphism and everything for this. But your approach of using JsonConverterAttribute on properties solved my confusion. If I had to make a JsonConverterFactory or apply the attribute to the class, I would have to write and read the Json manually. But with this I can trick the JsonSerializer to do all the work instead. I also figured out how to make it work with lists.

    That being said, I modified your approach to meet my criteria. Here is my code,

    using System.Collections;
    using System.Diagnostics;
    using System.Text.Json.Serialization;
    using System.Text.Json;
    
    namespace Symptum.Core.Serialization;
    
    public class Temp
    {
        public static void JsonTest()
        {
            A a = new()
            {
                Title = "a",
                X1 = new()
                {
                    Title = "x1",
                    LargeList =
                    [
                        new()
                        {
                            Title = "x1-y1",
                            AnotherList = ["placeholder"],
                            SplitMetadata = true,
                        },
                        new()
                        {
                            Title = "x1-y2",
                            AnotherList = ["placeholder"]
                        }
                    ],
                    SplitMetadata = true
                },
                X2 = new()
                {
                    Title = "x2",
                    LargeList =
                    [
                        new()
                        {
                            Title = "x2-y1",
                            AnotherList = ["dummy"]
                        },
                        new()
                        {
                            Title = "x2-y2",
                            AnotherList = ["dummy"]
                        }
                    ],
                    SplitMetadata = true
                },
                X3 = new()
                {
                    Title = "x3",
                    LargeList =
                    [
                        new()
                        {
                            Title = "x3-y1",
                            AnotherList = ["text"]
                        }
                    ]
                }
            };
    
            JsonSerializerOptions options = new() { WriteIndented = true };
            Debug.Write("\a.json");
            Debug.WriteLine("--------------------------------------");
            Debug.WriteLine(JsonSerializer.Serialize(a, options));
            Debug.Write("\a.json");
            Debug.WriteLine("--------------------------------------");
        }
    }
    
    public class A : Base
    {
        [Base]
        public X? X1 { get; set; }
    
        [Base]
        public X? X2 { get; set; }
    
        [Base]
        public X? X3 { get; set; }
    }
    
    public class X : Base
    {
        [ListOfBase]
        public List<Y>? LargeList { get; set; }
    }
    
    public class Y : Base
    {
        public List<string>? AnotherList { get; set; }
    }
    
    public class Base
    {
        public string? Title { get; set; }
    
        [JsonIgnore]
        public bool SplitMetadata { get; set; } = false;
    
        [JsonIgnore]
        public string? MetadataPath { get; set; }
    
        [JsonIgnore]
        public bool IsMetadataLoaded { get; set; } = true;
    }
    
    public class BaseConverter<T> : JsonConverter<T> where T : Base
    {
        public override T? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
        {
            if (reader.TokenType == JsonTokenType.String)
            {
                string json = reader.GetString() ?? string.Empty;
                if (json.StartsWith('\'))
                {
                    string filePath = json;
                    Debug.WriteLine(filePath);
                    if (Activator.CreateInstance(typeof(T)) is T obj)
                    {
                        obj.SplitMetadata = true;
                        obj.MetadataPath = filePath;
                        obj.IsMetadataLoaded = false;
                        return obj;
                    }
                }
            }
    
            return JsonSerializer.Deserialize<T>(ref reader, options);
        }
    
        public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options)
        {
            if (value == null) return;
    
            if (value.SplitMetadata)
            {
                string filePath = "\" + value.Title + ".json";
                writer.WriteStringValue(filePath);
                value.MetadataPath = filePath;
                Debug.Write(filePath + ":");
                Debug.WriteLine("--------------------------------------");
                Debug.WriteLine(JsonSerializer.Serialize(value, options));
                Debug.Write(filePath + ":");
                Debug.WriteLine("--------------------------------------");
            }
            else
            {
                JsonSerializer.Serialize(writer, value, options);
            }
        }
    }
    
    public class ListOfBaseConverter<TList, TBase> : JsonConverter<TList> where TList : IList<TBase> where TBase : Base
    {
        public override TList? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
        {
            if (reader.TokenType != JsonTokenType.StartArray)
            {
                throw new JsonException();
            }
    
            if (Activator.CreateInstance(typeToConvert) is TList list)
            {
                while (reader.Read())
                {
                    if (reader.TokenType == JsonTokenType.EndArray)
                    {
                        return list;
                    }
    
                    if (reader.TokenType == JsonTokenType.String)
                    {
                        string json = reader.GetString() ?? string.Empty;
                        if (json.StartsWith('\'))
                        {
                            string filePath = json;
                            Debug.WriteLine(filePath);
                            if (Activator.CreateInstance(typeof(TBase)) is TBase obj)
                            {
                                obj.SplitMetadata = true;
                                obj.MetadataPath = filePath;
                                obj.IsMetadataLoaded = false;
                                list.Add(obj);
                            }
                        }
                    }
                    else if (JsonSerializer.Deserialize<TBase>(ref reader, options) is TBase obj)
                    {
                        list.Add(obj);
                    }
                }
            }
    
    
            throw new JsonException();
        }
    
        public override void Write(Utf8JsonWriter writer, TList value, JsonSerializerOptions options)
        {
            writer.WriteStartArray();
    
            foreach (TBase item in value)
            {
                if (item == null) return;
    
                if (item.SplitMetadata)
                {
                    string filePath = "\" + item.Title + ".json";
                    writer.WriteStringValue(filePath);
                    item.MetadataPath = filePath;
                    Debug.Write(filePath + ":");
                    Debug.WriteLine("--------------------------------------");
                    Debug.WriteLine(JsonSerializer.Serialize(item, options));
                    Debug.Write(filePath + ":");
                    Debug.WriteLine("--------------------------------------");
                }
                else
                {
                    JsonSerializer.Serialize(writer, item, options);
                }
            }
    
            writer.WriteEndArray();
        }
    }
    
    [AttributeUsage(AttributeTargets.Property, AllowMultiple = false)]
    public class BaseAttribute : JsonConverterAttribute
    {
        public override JsonConverter? CreateConverter(Type typeToConvert)
        {
            if (!typeof(Base).IsAssignableFrom(typeToConvert))
                throw new NotSupportedException();
    
            return Activator.CreateInstance(typeof(BaseConverter<>).MakeGenericType([typeToConvert])) as JsonConverter;
        }
    }
    
    [AttributeUsage(AttributeTargets.Property, AllowMultiple = false)]
    public class ListOfBaseAttribute : JsonConverterAttribute
    {
        public override JsonConverter? CreateConverter(Type typeToConvert)
        {
            if (!typeToConvert.IsGenericType)
                throw new NotSupportedException();
    
            if (!typeof(IList).IsAssignableFrom(typeToConvert))
                throw new NotSupportedException();
    
            Type elementType = typeToConvert.GetGenericArguments()[0];
    
            if (!typeof(Base).IsAssignableFrom(elementType))
                throw new NotSupportedException();
    
            return Activator.CreateInstance(typeof(ListOfBaseConverter<,>).MakeGenericType([typeToConvert, elementType])) as JsonConverter;
        }
    }
    

    And here is the output:

    a.json--------------------------------------
    x1.json:--------------------------------------
    x1-y1.json:--------------------------------------
    {
      "AnotherList": [
        "placeholder"
      ],
      "Title": "x1-y1"
    }
    x1-y1.json:--------------------------------------
    {
      "LargeList": [
        "\x1-y1.json",
        {
          "AnotherList": [
            "placeholder"
          ],
          "Title": "x1-y2"
        }
      ],
      "Title": "x1"
    }
    x1.json:--------------------------------------
    x2.json:--------------------------------------
    {
      "LargeList": [
        {
          "AnotherList": [
            "dummy"
          ],
          "Title": "x2-y1"
        },
        {
          "AnotherList": [
            "dummy"
          ],
          "Title": "x2-y2"
        }
      ],
      "Title": "x2"
    }
    x2.json:--------------------------------------
    {
      "X1": "\x1.json",
      "X2": "\x2.json",
      "X3": {
        "LargeList": [
          {
            "AnotherList": [
              "text"
            ],
            "Title": "x3-y1"
          }
        ],
        "Title": "x3"
      },
      "Title": "a"
    }
    a.json--------------------------------------
    

    And as to support delayed loading (that is due to async file operations in UWP). This converter will return an empty instance with only the json file path in MetadataPath.

    And this empty instance can then be populated with the loaded json whenever needed. Here is the code for the populate method: https://github.com/dotnet/runtime/issues/29538#issuecomment-1330494636


  2. You can use json converters to customize produced json and perform custom steps (creating separate json-files in your case).

    Just to get you started, here is a converter:

    public class XConverter : JsonConverter<X>
    {
        public required string FileName { get; init; }
    
        public override X? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) =>
            JsonSerializer.Deserialize<X>(File.ReadAllText(reader.GetString()));
    
        public override void Write(Utf8JsonWriter writer, X value, JsonSerializerOptions options)
        {
            writer.WriteStringValue(FileName);
            File.WriteAllText(FileName, JsonSerializer.Serialize(value));
        }
    }
    

    To pass file name into converter you need to create attribute yourself:

    [AttributeUsage(AttributeTargets.Property)]
    public class JsonXConverterAttribute : JsonConverterAttribute
    {
        public required string FileName { get; init; }
    
        public override JsonConverter? CreateConverter(Type typeToConvert)
        {
            if (typeToConvert != typeof(X))
                throw new NotSupportedException("Attribute is only applicable to property of X type");
            return new XConverter { FileName = FileName };
        }
    }
    

    Now you can use attribute in your class:

    public class A
    {
        [JsonXConverter(FileName = "X1.json")]
        public X X1 { get; set; }
    
        [JsonXConverter(FileName = "X2.json")]
        public X X2 { get; set; }
    
        [JsonXConverter(FileName = "X3.json")]
        public X X3 { get; set; }
    }
    

    And to test we serialize and deserialize:

    A a = ... (your code)
    var json = JsonSerializer.Serialize(a);
    Debug.WriteLine(json);
    var b = JsonSerializer.Deserialize<A>(json);
    Debug.WriteLine(JsonSerializer.Serialize(b.X1));
    Debug.WriteLine(JsonSerializer.Serialize(b.X2));
    Debug.WriteLine(JsonSerializer.Serialize(b.X3));
    

    The output:

    10:48:54:929    {"X1":"X1.json","X2":"X2.json","X3":"X3.json"}
    10:48:54:929    {"LargeList":[{"AnotherList":["placeholder"]}]}
    10:48:54:929    {"LargeList":[{"AnotherList":["dummy"]}]}
    10:48:54:929    {"LargeList":[{"AnotherList":["text"]}]}
    

    And there should be 3 json-files near executable file.

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search