Search code examples
c#appsettingsjsonconverter

Writing a custom converter for appsettings.json


I want to create an appsettings.json converter which converts Symbols to IReadOnlyCollection<Symbol>. The converter should split the string by /, which will result into a BaseAsset/QuoteAsset. It should then check whether QuoteAsset equals to StakeCurrency or not. If it doesn't, throw an exception. What is the best way to do that with a custom converter? I don't want to use binding. Is it possible to use a custom JsonConverter?

  • appsettings.json
{
  "BacktestConfiguration": {
    "StakeCurrency": "USDT",
    "Symbols": [ "TRX/USDT", "BTC/USDT", "ETH/USDT" ]
  }
}
  • Classes
public class BacktestOptions
{
    public const string Position = "BacktestConfiguration";

    public string StakeCurrency { get; set; }
    public IReadOnlyCollection<Symbol> Symbols { get; set; }
}

public class Symbol
{
    public string BaseAsset { get; set; }
    public string QuoteAsset { get; set; }
}

Solution

  • You can use a custom JsonConverter to handle the transformation from the Array of strings to a IReadOnlyCollection<Symbol>.

    Decorate the Symbols property of the BacktestOptions class with the converter Type and handle the conversion in the custom converter's Read() method, where you split the strings in the array to generate new Symbol objects with the parts.
    Then return a new ReadOnlyCollection<Symbol> from the generated list of Symbol objects.

    I'm using a handler class, BacktestConfigurationHandler, to contain the objects and provide the base conversion and deserialization functionality.

    Call the static Deserialize() method, passing the JSON as argument. It returns a BacktestConfiguration object when there's no mismatch between the StakeCurrency value and any of the Symbol[].QuoteAsset values.
    It throws a JsonException in case there's a mismatch.

    Call it as:

    var configuration = BacktestConfigurationHandler.Deserialize(json);
    

    BacktestConfigurationHandler class:

    ► It handles deserialization only. The serialization, as you can see, is not implemented: the Write() method does nothing.

    public class BacktestConfigurationHandler
    {
        public class BacktestRoot {
            public BacktestConfiguration BacktestConfiguration { get; set; }
        }
    
        public class BacktestConfiguration
        {
            public const string Position = "BacktestConfiguration";
    
            public string StakeCurrency { get; set; }
    
            [JsonConverter(typeof(SymbolConverter))]
            public IReadOnlyCollection<Symbol> Symbols { get; set; }
        }
    
        public class Symbol
        {
            public Symbol() : this("", "") { }
            public Symbol(string baseAsset, string quoteAsset) {
                BaseAsset = baseAsset;
                QuoteAsset = quoteAsset;
            }
    
            public string BaseAsset { get; set; }
            public string QuoteAsset { get; set; }
        }
    
        public static BacktestConfiguration Deserialize(string json)
        {
            var root = JsonSerializer.Deserialize<BacktestRoot>(json);
            var stakeCurrency = root.BacktestConfiguration.StakeCurrency;
            if (root.BacktestConfiguration.Symbols.Any(s => s.QuoteAsset != stakeCurrency)) {
                throw new JsonException("StakeCurrency mismatch");
            }
            return root.BacktestConfiguration;
        }
    
        public class SymbolConverter : JsonConverter<IReadOnlyCollection<Symbol>>
        {
            public override IReadOnlyCollection<Symbol> Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
            {
                if (reader.TokenType == JsonTokenType.StartArray) {
                    var symbols = new List<Symbol>();
                    var values = JsonSerializer.Deserialize<string[]>(ref reader, options);
    
                    foreach (string value in values) {
                        var parts = value.Split('/');
                        symbols.Add(new Symbol(parts[0], parts[1]));
    
                    }
                    return new ReadOnlyCollection<Symbol>(symbols);
                }
                return null;
    
            public override void Write(Utf8JsonWriter writer, IReadOnlyCollection<Symbol> value, JsonSerializerOptions options)
                => throw new NotImplementedException();
        }
    }
    

    EDIT:
    Try these variations of the class Model and the adapted custom converters, both a JSON converter and a TypeConverter assigned to the Symbols Property.

    I've tested the converters in a base scenario, converting from JSON to a class model directly and accessing / casting the objects using the implicit operators called by the TypeConverter.

    public class BacktestOptionsRoot
    {
        public BacktestOptions BacktestConfiguration { get; set; }
    }
    
    public class BacktestOptions
    {
        public const string Position = "BacktestConfiguration";
    
        public string StakeCurrency { get; set; }
    
        [JsonConverter(typeof(JsonSymbolConverter))]
        [TypeConverter(typeof(BacktestSymbolsConverter))]
        public BacktestSymbols Symbols { get; set; }
    }
    
    public class Symbol
    {
        public Symbol() : this("", "") { }
        public Symbol(string baseAsset, string quoteAsset)
        {
            BaseAsset = baseAsset;
            QuoteAsset = quoteAsset;
        }
    
        public string BaseAsset { get; set; }
        public string QuoteAsset { get; set; }
    }
    
    public class BacktestSymbolsConverter : TypeConverter
    {
        public override object ConvertTo(ITypeDescriptorContext context, CultureInfo culture, object value, Type destinationType)
        {
            if (destinationType == typeof(string[])) {
                return (BacktestSymbols)(value as string[]);
            }
            return base.ConvertTo(context, culture, value, destinationType);
        }
    
        public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value)
        {
            if (value is BacktestSymbols) {
                return (string[])(value as BacktestSymbols);
            }
            return base.ConvertFrom(context, culture, value);
        }
    }
    
    public class JsonSymbolConverter : JsonConverter<BacktestSymbols>
    {
        public override bool CanConvert(Type typeToConvert)
        {
            return typeToConvert == typeof(BacktestSymbols);
        }
    
        public override BacktestSymbols Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
        {
            if (reader.TokenType == JsonTokenType.StartArray) {
                return JsonSerializer.Deserialize<string[]>(ref reader, options);
            }
            return null;
        }
    
        public override void Write(Utf8JsonWriter writer, BacktestSymbols value, JsonSerializerOptions options)
        {
            throw new NotImplementedException();
        }
    }
    
    
    public class BacktestSymbols
    {
        public ReadOnlyCollection<Symbol> Value { get; }
        public string[] Symbols => Value.Select(v => $"{v.BaseAsset}/{v.QuoteAsset}").ToArray();
    
        public BacktestSymbols(string[] source)
        {
            var symbols = new List<Symbol>();
    
            if (source != null && source.Length > 0) {
                foreach (string value in source) {
                    var parts = value.Split('/');
                    symbols.Add(new Symbol(parts[0], parts[1]));
                }
            }
            Value = new ReadOnlyCollection<Symbol>(symbols);
        }
    
        public static implicit operator BacktestSymbols(string[] source) => new BacktestSymbols(source);
        public static implicit operator string[](BacktestSymbols instance) => instance.Symbols;
        public override string ToString() => $"[ {string.Join(",", Symbols)} ]";
    }