Created
April 18, 2022 20:11
-
-
Save mbcrawfo/1655e6eded307383975895b8470c8288 to your computer and use it in GitHub Desktop.
Custom JsonConverter for Dynamic Property Types
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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