Skip to content

Instantly share code, notes, and snippets.

@mbcrawfo
Created April 18, 2022 20:11
Show Gist options
  • Save mbcrawfo/1655e6eded307383975895b8470c8288 to your computer and use it in GitHub Desktop.
Save mbcrawfo/1655e6eded307383975895b8470c8288 to your computer and use it in GitHub Desktop.
Custom JsonConverter for Dynamic Property Types
using System;
using System.Collections.Generic;
using FluentAssertions;
using Newtonsoft.Json;
using Xunit;
namespace TestProject1;
public enum PricingState
{
Simple,
Detailed
}
public record PricingData(
[property: JsonConverter(typeof(NullOrEmptyStringAsZeroConverter))] decimal Points,
[property: JsonConverter(typeof(NullOrEmptyStringAsZeroConverter))] decimal Single,
[property: JsonConverter(typeof(NullOrEmptyStringAsZeroConverter))] decimal Double
);
public record PricingDetail(PricingData Whole, PricingData Half);
[JsonConverter(typeof(PricingConverter))]
public record Pricing(PricingState State, PricingDetail? Detail, decimal? Price);
public record PriceList(Dictionary<string, Pricing> Prices);
internal class NullOrEmptyStringAsZeroConverter : JsonConverter<decimal>
{
/// <inheritdoc />
public override void WriteJson(JsonWriter writer, decimal value, JsonSerializer serializer)
{
throw new NotImplementedException();
}
/// <inheritdoc />
public override decimal ReadJson(
JsonReader reader,
Type objectType,
decimal existingValue,
bool hasExistingValue,
JsonSerializer serializer
)
{
switch (reader.TokenType)
{
case JsonToken.Null:
return 0;
case JsonToken.String:
var stringValue = reader.Value as string;
if (string.IsNullOrEmpty(stringValue))
{
return 0;
}
return decimal.Parse(stringValue);
case JsonToken.Float:
case JsonToken.Integer:
return serializer.Deserialize<decimal>(reader);
default:
throw new InvalidOperationException("Unexpected json token " + reader.TokenType);
}
}
}
internal class PricingConverter : JsonConverter<Pricing>
{
private readonly NullOrEmptyStringAsZeroConverter _decimalConverter = new();
/// <inheritdoc />
public override void WriteJson(JsonWriter writer, Pricing? value, JsonSerializer serializer)
{
throw new NotImplementedException();
}
/// <inheritdoc />
public override Pricing? ReadJson(
JsonReader reader,
Type objectType,
Pricing? existingValue,
bool hasExistingValue,
JsonSerializer serializer
)
{
switch (reader.TokenType)
{
case JsonToken.Null:
return new(PricingState.Simple, null, 0);
case JsonToken.String:
case JsonToken.Float:
case JsonToken.Integer:
return new(
PricingState.Simple,
null,
_decimalConverter.ReadJson(reader, typeof(decimal), 0, false, serializer)
);
case JsonToken.StartObject:
return new(
PricingState.Detailed,
serializer.Deserialize<PricingDetail>(reader)!,
null
);
default:
throw new InvalidOperationException("Unexpected json token " + reader.TokenType);
}
}
}
public class PricingDeserializationTests
{
[Fact]
public void ShouldDeserializeDetailsWhenNumbers()
{
const string json = @"
{
""prices"": {
""b4Z7ml1jq"": {
""whole"": {
""points"": 1,
""single"": 0,
""double"": 1.5
},
""half"": {
""points"": 0.5,
""single"": 0,
""double"": 0.75
}
}
}
}";
var result = JsonConvert.DeserializeObject<PriceList>(json);
result.Prices.Should()
.HaveCount(1)
.And.ContainKey("b4Z7ml1jq")
.And.ContainValue(
new(
PricingState.Detailed,
new(new(1, 0, 1.5m), new(0.5m, 0, 0.75m)),
null
)
);
}
[Fact]
public void ShouldDeserializeDetails_WhenStrings()
{
const string json = @"
{
""prices"": {
""b4Z7ml1jq"": {
""whole"": {
""points"": ""1"",
""single"": ""3"",
""double"": ""6""
},
""half"": {
""points"": ""1"",
""single"": ""1.5"",
""double"": ""3""
}
}
}
}";
var result = JsonConvert.DeserializeObject<PriceList>(json);
result.Prices.Should()
.HaveCount(1)
.And.ContainKey("b4Z7ml1jq")
.And.ContainValue(new(PricingState.Detailed, new(new(1, 3, 6), new(1, 1.5m, 3)), null));
}
[Fact]
public void ShouldDeserializeDetails_WhenEmptyStrings()
{
const string json = @"
{
""prices"": {
""b4Z7ml1jq"": {
""whole"": {
""points"": """",
""single"": """",
""double"": """"
},
""half"": {
""points"": """",
""single"": """",
""double"": """"
}
}
}
}";
var result = JsonConvert.DeserializeObject<PriceList>(json);
result.Prices.Should()
.HaveCount(1)
.And.ContainKey("b4Z7ml1jq")
.And.ContainValue(new(PricingState.Detailed, new(new(0, 0, 0), new(0, 0, 0)), null));
}
[Fact]
public void ShouldDeserializeDetails_WhenNull()
{
const string json = @"
{
""prices"": {
""b4Z7ml1jq"": {
""whole"": {
""points"": null,
""single"": null,
""double"": null
},
""half"": {
""points"": null,
""single"": null,
""double"": null
}
}
}
}";
var result = JsonConvert.DeserializeObject<PriceList>(json);
result.Prices.Should()
.HaveCount(1)
.And.ContainKey("b4Z7ml1jq")
.And.ContainValue(new(PricingState.Detailed, new(new(0, 0, 0), new(0, 0, 0)), null));
}
[Fact]
public void ShouldDeserializeSimple_WhenString()
{
const string json = @"
{
""prices"": {
""EW3H87b8s"": ""3""
}
}";
var result = JsonConvert.DeserializeObject<PriceList>(json);
result.Prices.Should()
.HaveCount(1)
.And.ContainKey("EW3H87b8s")
.And.ContainValue(new(PricingState.Simple, null, 3));
}
[Fact]
public void ShouldDeserializeSimple_WhenNumber()
{
const string json = @"
{
""prices"": {
""EW3H87b8s"": 3.5
}
}";
var result = JsonConvert.DeserializeObject<PriceList>(json);
result.Prices.Should()
.HaveCount(1)
.And.ContainKey("EW3H87b8s")
.And.ContainValue(new(PricingState.Simple, null, 3.5m));
}
[Fact]
public void ShouldDeserializeSimple_WhenEmpty()
{
const string json = @"
{
""prices"": {
""EW3H87b8s"": """"
}
}";
var result = JsonConvert.DeserializeObject<PriceList>(json);
result.Prices.Should()
.HaveCount(1)
.And.ContainKey("EW3H87b8s")
.And.ContainValue(new(PricingState.Simple, null, 0));
}
[Fact]
public void ShouldDeserializeSimple_WhenNull()
{
const string json = @"
{
""prices"": {
""EW3H87b8s"": null
}
}";
var result = JsonConvert.DeserializeObject<PriceList>(json);
result.Prices.Should()
.HaveCount(1)
.And.ContainKey("EW3H87b8s")
.And.ContainValue(new(PricingState.Simple, null, 0));
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment