Search code examples
c#powershelljson.netjsonconverter

How to serialize/deserialize json dictionary into array


Need to serialize and deserialize C# dictionaries into a JSON array. I would like to also read the JSON from powershell using array index notation.

By default, JSON format is:

{
 "defaultSettings": {
  "applications": {
   "Apollo": {
    "environments": {
      "DEV": {
        "dbKeyTypes": {
          "DmkPassword": "AEikOooIuGxXC9UBJQ3ckDj7Q126tB06",
          "SymmetricKeySource": "bTU7XOAYA2FFifmiBUggu99yHxX3Ftds"
        }
      },
      "TST": {
        "dbKeyTypes": {
          "DmkPassword": "AEikOooIuGxXC9UBJQ3ckDj7Q126tB06",
          "SymmetricKeySource": "bTU7XOAYA2FFifmiBUggu99yHxX3Ftds"
        }
      }
    }
  },
  "Gemini": {
    "environments": {
      "DEV": {
        "dbKeyTypes": {
          "DmkPassword": "AEikOooIuGxXC9UBJQ3ckDj7Q126tB06",
          "SymmetricKeySource": "bTU7XOAYA2FFifmiBUggu99yHxX3Ftds"
        }
      },
      "TST": {
        "dbKeyTypes": {
          "DmkPassword": "AEikOooIuGxXC9UBJQ3ckDj7Q126tB06",
          "SymmetricKeySource": "bTU7XOAYA2FFifmiBUggu99yHxX3Ftds"
        }
      }
    }
   }
  }
 }
}

This works great using the default json reader in .Net Core, but it doesn't allow me to use array index notation in PowerShell.

Instead what I'm looking for is this:

{
 "defaultSettings": {
  "applications": [
   {
     "Apollo": {
      "environments": [
        {
          "DEV": {
            "dbKeyTypes": [
              {
                "DmkPassword": "AEikOooIuGxXC9UBJQ3ckDj7Q126tB06"
              },
              {
                "SymmetricKeySource": "bTU7XOAYA2FFifmiBUggu99yHxX3Ftds"
              }
            ]
          }
        },
        {
          "TST": {
            "dbKeyTypes": [
              {
                "DmkPassword": "AEikOooIuGxXC9UBJQ3ckDj7Q126tB06"
              },
              {
                "SymmetricKeySource": "bTU7XOAYA2FFifmiBUggu99yHxX3Ftds"
              }
            ]
          }
        }
      ]
    }
  },
  {
    "Gemini": {
      "environments": [
        {
          "DEV": {
            "dbKeyTypes": [
              {
                "DmkPassword": "AEikOooIuGxXC9UBJQ3ckDj7Q126tB06"
              },
              {
                "SymmetricKeySource": "bTU7XOAYA2FFifmiBUggu99yHxX3Ftds"
              }
            ]
          }
        },
        {
          "TST": {
            "dbKeyTypes": [
              {
                "DmkPassword": "AEikOooIuGxXC9UBJQ3ckDj7Q126tB06"
              },
              {
                "SymmetricKeySource": "bTU7XOAYA2FFifmiBUggu99yHxX3Ftds"
              }
            ]
          }
        }
      ]
    }
   }
  ]
 }
}

I'm using the WriteJson part from Serializing Dictionary<string,string> to array of "name": "value"

This works well; however, of course since the ReadJson() method isn't implemented, it doesn't read. Btw, to get the above desired json format, I modified the CustomDictionaryConverter in the link to:

writer.WritePropertyName(key.ToString());
//writer.WriteValue(key);
//writer.WritePropertyName("value");
serializer.Serialize(writer, valueEnumerator.Current);

The classes behind the implementation are:

public enum DeploymentEnvironment { DEV = 1, TST = 2 }
public enum TargetApplication { Apollo = 1, Gemini = 2 }
public enum DbKeyType { DmkPassword = 1, SymmetricKeySource = 2 }

public class DeploymentSettings
{
    [JsonProperty("defaultSettings")]
    public DefaultSettings DefaultSettings { get; set; }
    public DeploymentSettings()
    {
        DefaultSettings = new DefaultSettings();
    }
}

public partial class DefaultSettings
{
    [JsonProperty("applications")]
    public Dictionary<TargetApplication, ApplicationContainer> Applications { get; set; }

    public DefaultSettings()
    {
        Applications = new Dictionary<TargetApplication, ApplicationContainer>();
    }
}

public partial class ApplicationContainer
{
    [JsonProperty("environments")]
    public Dictionary<DeploymentEnvironment, EnvironmentContainer> Environments { get; set; }
    public ApplicationContainer()
    {
        Environments = new Dictionary<DeploymentEnvironment, EnvironmentContainer>();
    }
}

public partial class EnvironmentContainer
{
    [JsonProperty("dbKeyTypes")]
    public Dictionary<DbKeyType, string> DbKeyTypes { get; set; }

    public EnvironmentContainer()
    {
        DbKeyTypes = new Dictionary<DbKeyType, string>();
    }
}

I'm serializing the object as follows: var json = JsonConvert.SerializeObject(ds, Formatting.Indented, new CustomDictionaryConverter());

As mentioned, serializing works, but I need help writing the ReadJson() method in order to be able to deserialize.


Solution

  • You can extend CustomDictionaryConverter to read as well as write as follows:

    public class CustomDictionaryConverter : JsonConverter
    {
        // Adapted from CustomDictionaryConverter from this answer https://stackoverflow.com/a/40265708
        // To https://stackoverflow.com/questions/40257262/serializing-dictionarystring-string-to-array-of-name-value
        // By Brian Rogers https://stackoverflow.com/users/10263/brian-rogers
    
        sealed class InconvertibleDictionary : Dictionary<object, object>
        {
            public InconvertibleDictionary(DictionaryEntry entry)
                : base(1)
            {
                this[entry.Key] = entry.Value;
            }
        }
    
        public override bool CanConvert(Type objectType)
        {
            return typeof(IDictionary).IsAssignableFrom(objectType) && objectType != typeof(InconvertibleDictionary);
        }
    
        public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
        {
            // Lazy evaluation of the enumerable prevents materialization of the entire collection of dictionaries at once.
            serializer.Serialize(writer,  Entries(((IDictionary)value)).Select(p => new InconvertibleDictionary(p)));
        }
    
        public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
        {
            if (reader.MoveToContentAndAssert().TokenType == JsonToken.Null)
                return null;
            var dictionary = existingValue ?? serializer.ContractResolver.ResolveContract(objectType).DefaultCreator();
            switch (reader.TokenType)
            {
                case JsonToken.StartObject:
                    serializer.Populate(reader, dictionary);
                    return dictionary;
    
                case JsonToken.StartArray:
                    {
                        while (true)
                        {
                            switch (reader.ReadToContentAndAssert().TokenType)
                            {
                                case JsonToken.EndArray:
                                    return dictionary;
    
                                case JsonToken.StartObject:
                                    serializer.Populate(reader, dictionary);
                                    break;
    
                                default:
                                    throw new JsonSerializationException(string.Format("Unexpected token {0}", reader.TokenType));
                            }
                        }
                    }
    
                default:
                    throw new JsonSerializationException(string.Format("Unexpected token {0}", reader.TokenType));
            }
        }
    
        static IEnumerable<DictionaryEntry> Entries(IDictionary dict)
        {
            foreach (DictionaryEntry entry in dict)
                yield return entry;
        }
    }
    
    public static partial class JsonExtensions
    {
        public static JsonReader ReadToContentAndAssert(this JsonReader reader)
        {
            return reader.ReadAndAssert().MoveToContentAndAssert();
        }
    
        public static JsonReader MoveToContentAndAssert(this JsonReader reader)
        {
            if (reader == null)
                throw new ArgumentNullException();
            if (reader.TokenType == JsonToken.None)       // Skip past beginning of stream.
                reader.ReadAndAssert();
            while (reader.TokenType == JsonToken.Comment) // Skip past comments.
                reader.ReadAndAssert();
            return reader;
        }
    
        public static JsonReader ReadAndAssert(this JsonReader reader)
        {
            if (reader == null)
                throw new ArgumentNullException();
            if (!reader.Read())
                throw new JsonReaderException("Unexpected end of JSON stream.");
            return reader;
        }
    }
    

    You can then serialize and deserialize your DeploymentSettings with the following settings:

    var settings = new JsonSerializerSettings
    {
        Converters = { new CustomDictionaryConverter(), new StringEnumConverter() }
    };
    
    var ds = JsonConvert.DeserializeObject<DeploymentSettings>(json, settings);
    
    var json2 = JsonConvert.SerializeObject(ds, Formatting.Indented, settings);
    

    Notes:

    • This version of the converter avoids loading the entire dictionary into a temporary JArray hierarchy in either ReadJson() or WriteJson(), and instead streams directly from and to the JSON stream.

    • Because the serializer is now used to directly serialize the individual dictionary entries, StringEnumConverter is required for the keys to be named correctly. (Using the serializer also ensures that numeric or DateTime keys are properly internationalized, should you be using such a dictionary anywhere.)

    • Because Json.NET supports comments, the converter checks for and skips them, which adds a bit to the complexity. (I wish there were a way to make JsonReader silently skip over comments.)

    Demo fiddle here.