0

Using System.Text.Json, how can I serialize (and deserialize) an array of n elements (classes), to an object of n children, where the element name is one of the properties of the object?

For example, by default the following classes

public class Project
{
   public string Name {get;set;}
   public Environment[] Environments {get;set;}
}

public class Environment
{
   public string Name {get;set;}
   public string Region {get;set;}
   public Settings Settings {get;set;}
}
public class Settings
{
   public bool OverrideOnFetch {get;set;}
}

will be serialized as

{
  "name": "MyProject",  
  "environments": [
    {
      "name": "dev",
      "region": "us1",
      "settings": {
        "overrideOnFetch": false
      }
    },
    {
      "name": "prod",
      "region": "us1",
      "settings": {
        "overrideOnFetch": false
      }
    }
  ]
}

But I want to change this behavior and serialize it as

 {
   "name": "MyProject",  
   "environments": {
     "dev": {
       "region": "us1",
       "settings": {
         "overrideOnFetch": false
       }
     },
     "prod": {
       "region": "us1",
       "settings": {
         "overrideOnFetch": false
       }
     }
   }
 }

without changing the classes (or creating another). As you can see, the Environment.Name acts like the property of environments in JSON.

I have no idea where to continue from here

class ProjectJsonConverter : JsonConverter<Project>
{
   public override Project? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
   {
      throw new NotImplementedException();
   }

    public override void Write(Utf8JsonWriter writer, Project value, JsonSerializerOptions options)
    {
        var namePolicy = options.PropertyNamingPolicy ?? new DefaultJsonNamingPolicy();

        writer.WriteStartObject(); //starts the "project" object
        writer.WriteString(JsonEncodedText.Encode(namePolicy.ConvertName(nameof(value.Name))), value.Name);
        //How to write the "value.Settings" without rewriting everything?

        writer.WriteStartObject(); //starts the "environment" object
        foreach(var env in value.Environments)
        {

        }
        writer.WriteEndObject(); //ends the "environment" object

        writer.WriteEndObject(); //ends the "project" object
    }

    class DefaultJsonNamingPolicy : JsonNamingPolicy
    {
        public override string ConvertName(string name)
        {
            return name;
        }
    }
}
4
  • You can serialize it as Dictionary<string, Project> where each of the keys is a name such as dev Commented Aug 8, 2022 at 1:00
  • hum, that's interesting, but it does not solve everything because the Environment.Name property should not be be serialized. I'm now actually trying to make this generic, so that I create a converter for array of T and the converter serialize the array as dictionary<string, t>, given the property name to be the key and to be removed from the t serialization. Commented Aug 8, 2022 at 1:11
  • Yes you could make a converter to do this, but the problem is you have no way of specifying constructor parameters for it, so you can't tell it which property to remove and turn into a key. Will it always be name on all classes? Commented Aug 8, 2022 at 1:17
  • This can be accomplished with JsonConverterAttribute + my custom JsonConverter. In the attribute you specify the property name as string (using nameof) Commented Aug 8, 2022 at 2:11

1 Answer 1

3

A much simpler solution than manually writing the JSON array, is to serialize and deserialize to/from a Dictionary within your converter. Then simply convert to/from a list or array type.

A generic version of this would be:

public class PropertyToObjectListConverter<T> : JsonConverter<ICollection<T>>
{
    static PropertyInfo _nameProp =
        typeof(T).GetProperties().FirstOrDefault(p => p.GetCustomAttributes(typeof(JsonPropertyToObjectAttribute)).Any())
        ?? typeof(T).GetProperty("Name");

    static Func<T, string> _getter = _nameProp?.GetMethod?.CreateDelegate<Func<T, string>>() ?? throw new Exception("No getter available");
    static Action<T, string> _setter = _nameProp?.SetMethod?.CreateDelegate<Action<T, string>>() ?? throw new Exception("No setter available");

    public override bool CanConvert(Type typeToConvert) =>
        typeof(ICollection<T>).IsAssignableFrom(typeToConvert);

    public override ICollection<T> Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
    {
        var dict = JsonSerializer.Deserialize<Dictionary<string, T>>(ref reader, options);
        var coll = (ICollection<T>) Activator.CreateInstance(typeToConvert);
        foreach (var kvp in dict)
        {
            var value = kvp.Value;
            _setter(value, kvp.Key);
            coll.Add(value);
        }
        return coll;
    }
    
    public override void Write(Utf8JsonWriter writer, ICollection<T> value, JsonSerializerOptions options)
    {
        var dict = value.ToDictionary(_getter);
        JsonSerializer.Serialize(writer, dict, options);
    }
}

[AttributeUsage(AttributeTargets.Property)]
public class JsonPropertyToObjectAttribute : Attribute
{
}

It looks first for a property with the JsonPropertyToObject attribute, otherwise it tries to find a property called Name. It must be a string type.

Note the use of static getter and setter functions to make the reflection fast, this only works on properties, not fields.

You would use it like this:

public class Project
{
   public string Name {get;set;}
   [JsonConverter(typeof(PropertyToObjectListConverter<Environment>))]
   public Environment[] Environments {get;set;}
}

public class Environment
{
   [JsonIgnore]
   public string Name {get;set;}
   public string Region {get;set;}
   public Settings Settings {get;set;}
}

dotnetfiddle

Sign up to request clarification or add additional context in comments.

Comments

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.