-
Notifications
You must be signed in to change notification settings - Fork 14
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
untested json_net and test_json serializations
- Loading branch information
Showing
13 changed files
with
697 additions
and
1 deletion.
There are no files selected for viewing
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
190 changes: 190 additions & 0 deletions
190
src/NMoneys.Serialization/Json_NET/DescriptiveMoneyConverter.cs
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,190 @@ | ||
using System.Diagnostics.CodeAnalysis; | ||
using Newtonsoft.Json; | ||
using Newtonsoft.Json.Serialization; | ||
|
||
namespace NMoneys.Serialization.Json_NET; | ||
|
||
/// <summary> | ||
/// Converts an monetary quantity <see cref="NMoneys.Money"/> to and from JSON. | ||
/// </summary> | ||
/// <remarks> | ||
/// <para>The serialized quantity would look something like: <c>{"Amount": 0, "Currency": "XXX"}</c>.</para> | ||
/// <para>Provides better JSON pointers for deserialization errors: better suited when not in control of serialization.</para> | ||
/// </remarks> | ||
public class DescriptiveMoneyConverter : JsonConverter<Money> | ||
{ | ||
private readonly bool _forceStringEnum; | ||
|
||
/// <param name="forceStringEnum">Ignore enum value configuration and force string representation of the | ||
/// <see cref="Money.CurrencyCode"/> when serializing.</param> | ||
public DescriptiveMoneyConverter(bool forceStringEnum = false) | ||
{ | ||
_forceStringEnum = forceStringEnum; | ||
} | ||
|
||
#region write | ||
|
||
/// <inheritdoc /> | ||
public override void WriteJson([NotNull] JsonWriter writer, Money value, [NotNull] JsonSerializer serializer) | ||
{ | ||
DefaultContractResolver? resolver = serializer.ContractResolver as DefaultContractResolver; | ||
|
||
writer.WriteStartObject(); | ||
|
||
writeAmount(value.Amount, writer, resolver); | ||
writeCurrency(value.CurrencyCode, writer, serializer, resolver); | ||
|
||
writer.WriteEndObject(); | ||
} | ||
|
||
private static void writeAmount(decimal amount, JsonWriter writer, DefaultContractResolver? resolver) | ||
{ | ||
// non-pascal if "weird" resolver | ||
string amountName = resolver?.GetResolvedPropertyName("Amount") ?? "amount"; | ||
writer.WritePropertyName(amountName); | ||
writer.WriteValue(amount); | ||
} | ||
|
||
private void writeCurrency(CurrencyIsoCode currency, | ||
JsonWriter writer, JsonSerializer serializer, DefaultContractResolver? resolver) | ||
{ | ||
// non-pascal if "weird" resolver | ||
string currencyName = resolver?.GetResolvedPropertyName("Currency") ?? "currency"; | ||
writer.WritePropertyName(currencyName); | ||
if (_forceStringEnum) | ||
{ | ||
// ignore configured enum value convention, string it is | ||
writer.WriteValue(currency.ToString()); | ||
} | ||
else | ||
{ | ||
// follow configured enum value convention | ||
serializer.Serialize(writer, currency, typeof(CurrencyIsoCode)); | ||
} | ||
} | ||
|
||
#endregion | ||
|
||
#region read | ||
|
||
/// <inheritdoc /> | ||
public override Money ReadJson([NotNull] JsonReader reader, | ||
Type objectType, Money existingValue, | ||
bool hasExistingValue, [NotNull] JsonSerializer serializer) | ||
{ | ||
DefaultContractResolver? resolver = serializer.ContractResolver as DefaultContractResolver; | ||
|
||
// no need to read StartObject token (already read) | ||
|
||
decimal amount = readAmount(reader, resolver); | ||
CurrencyIsoCode currency = readCurrency(reader, serializer, resolver); | ||
|
||
// but EndObject needs to be read | ||
readEndObject(reader); | ||
|
||
return new Money(amount, currency); | ||
} | ||
|
||
private static decimal readAmount(JsonReader reader, DefaultContractResolver? resolver) | ||
{ | ||
readProperty(reader); | ||
ensurePropertyName("Amount", reader, resolver); | ||
decimal amount = readAmountValue(reader); | ||
return amount; | ||
} | ||
|
||
private static CurrencyIsoCode readCurrency(JsonReader reader, JsonSerializer serializer, DefaultContractResolver? resolver) | ||
{ | ||
readProperty(reader); | ||
ensurePropertyName("Currency", reader, resolver); | ||
CurrencyIsoCode currency = readCurrencyValue(reader, serializer); | ||
return currency; | ||
} | ||
|
||
private static void readEndObject(JsonReader reader) | ||
{ | ||
bool read = reader.Read(); | ||
if (!read || reader.TokenType != JsonToken.EndObject) | ||
{ | ||
throw buildException($"Expected token type '{JsonToken.EndObject}', but got '{reader.TokenType}'.", reader); | ||
} | ||
} | ||
|
||
private static JsonSerializationException buildException(string message, JsonReader reader, Exception? inner = null) | ||
{ | ||
IJsonLineInfo? info = reader as IJsonLineInfo; | ||
JsonSerializationException exception = (info == null || info.HasLineInfo()) ? | ||
new JsonSerializationException(message) : | ||
new JsonSerializationException(message, reader.Path, info.LineNumber, info.LinePosition, inner); | ||
return exception; | ||
} | ||
|
||
|
||
private static void readProperty(JsonReader reader) | ||
{ | ||
bool isRead = reader.Read(); | ||
if (!isRead || reader.TokenType != JsonToken.PropertyName) | ||
{ | ||
throw buildException($"Expected token type '{JsonToken.PropertyName}', but got '{reader.TokenType}'.", reader); | ||
} | ||
} | ||
|
||
private static void ensurePropertyName(string pascalSingleName, JsonReader reader, DefaultContractResolver? resolver) | ||
{ | ||
#pragma warning disable CA1308 | ||
string propName = resolver?.GetResolvedPropertyName(pascalSingleName) ?? pascalSingleName.ToLowerInvariant(); | ||
#pragma warning restore CA1308 | ||
bool matchAmount = StringComparer.Ordinal.Equals(reader.Value, propName); | ||
if (!matchAmount) | ||
{ | ||
throw buildException($"Expected property '{propName}', but got '{reader.Value}'.", reader); | ||
} | ||
} | ||
|
||
private static decimal readAmountValue(JsonReader reader) | ||
{ | ||
try | ||
{ | ||
var amount = reader.ReadAsDecimal(); | ||
if (!amount.HasValue) | ||
{ | ||
throw buildException("Amount should not be nullable.", reader); | ||
} | ||
|
||
return amount.Value; | ||
} | ||
catch (Exception ex) | ||
{ | ||
throw buildException("Could not read amount value.", reader, ex); | ||
} | ||
} | ||
|
||
private static CurrencyIsoCode readCurrencyValue(JsonReader reader, JsonSerializer serializer) | ||
{ | ||
bool read = reader.Read(); | ||
if (!read) | ||
{ | ||
throw buildException("Expected value token type.", reader); | ||
} | ||
|
||
CurrencyIsoCode currency = serializer.Deserialize<CurrencyIsoCode>(reader); | ||
ensureDefined(currency, reader); | ||
|
||
return currency; | ||
} | ||
|
||
private static void ensureDefined(CurrencyIsoCode maybeCurrency, JsonReader reader) | ||
{ | ||
try | ||
{ | ||
maybeCurrency.AssertDefined(); | ||
} | ||
catch (Exception ex) | ||
{ | ||
throw buildException($"Currency '{maybeCurrency}' not defined.", reader, ex); | ||
} | ||
} | ||
|
||
|
||
#endregion | ||
} |
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,119 @@ | ||
using System.Diagnostics.CodeAnalysis; | ||
using Newtonsoft.Json; | ||
using Newtonsoft.Json.Linq; | ||
using Newtonsoft.Json.Serialization; | ||
|
||
namespace NMoneys.Serialization.Json_NET; | ||
|
||
/// <summary> | ||
/// Converts an monetary quantity <see cref="NMoneys.Money"/> to and from JSON. | ||
/// </summary> | ||
/// <remarks> | ||
/// <para>The serialized quantity would look something like: <c>{"Amount": 0, "Currency": "XXX"}</c>.</para> | ||
/// <para>Provides limited JSON pointers for deserialization errors: better suited when in control of serialization.</para> | ||
/// </remarks> | ||
public class MoneyConverter : JsonConverter<Money> | ||
{ | ||
private readonly bool _forceStringEnum; | ||
|
||
/// <param name="forceStringEnum">Ignore enum value configuration and force string representation of the | ||
/// <see cref="Money.CurrencyCode"/> when serializing.</param> | ||
public MoneyConverter(bool forceStringEnum = false) | ||
{ | ||
_forceStringEnum = forceStringEnum; | ||
} | ||
|
||
#region read | ||
|
||
/// <inheritdoc /> | ||
public override Money ReadJson([NotNull] JsonReader reader, | ||
Type objectType, Money existingValue, | ||
bool hasExistingValue, [NotNull] JsonSerializer serializer) | ||
{ | ||
JObject obj = readObject(reader); | ||
decimal amount = getAmount(obj); | ||
CurrencyIsoCode currency = getCurrency(obj); | ||
return new Money(amount, currency); | ||
} | ||
|
||
private static JObject readObject(JsonReader reader) | ||
{ | ||
JToken objToken = JToken.ReadFrom(reader); | ||
if (objToken.Type != JTokenType.Object) | ||
{ | ||
throw new JsonSerializationException($"Expected token '{JTokenType.Object}', but got '{objToken.Type}'."); | ||
} | ||
|
||
return (JObject)objToken; | ||
} | ||
|
||
private static decimal getAmount(JObject obj) | ||
{ | ||
string propName = "Amount"; | ||
JProperty amountProp = getProperty(obj, propName); | ||
decimal? amount = amountProp.Value.Value<decimal>(); | ||
return amount ?? throw new JsonSerializationException($"'{propName}' cannot be null."); | ||
} | ||
|
||
private static CurrencyIsoCode getCurrency(JObject obj) | ||
{ | ||
string propName = "Currency"; | ||
JProperty currencyProp = getProperty(obj, propName); | ||
var currency = currencyProp.Value.ToObject<CurrencyIsoCode>(); | ||
currency.AssertDefined(); | ||
return currency; | ||
} | ||
|
||
private static JProperty getProperty(JObject obj, string singleWordPropName) | ||
{ | ||
// since props are single-word, case ignoring cover most common: pascal, camel, snake, kebab | ||
JProperty? amountProp = obj.Property(singleWordPropName, StringComparison.OrdinalIgnoreCase) ?? | ||
throw new JsonSerializationException($"Missing property '{singleWordPropName}'."); | ||
return amountProp; | ||
} | ||
|
||
#endregion | ||
|
||
#region write | ||
|
||
/// <inheritdoc /> | ||
public override void WriteJson([NotNull] JsonWriter writer, Money value, [NotNull] JsonSerializer serializer) | ||
{ | ||
DefaultContractResolver? resolver = serializer.ContractResolver as DefaultContractResolver; | ||
|
||
writer.WriteStartObject(); | ||
|
||
writeAmount(value.Amount, writer, resolver); | ||
writeCurrency(value.CurrencyCode, writer, serializer, resolver); | ||
|
||
writer.WriteEndObject(); | ||
} | ||
|
||
private static void writeAmount(decimal amount, JsonWriter writer, DefaultContractResolver? resolver) | ||
{ | ||
// non-pascal if "weird" resolver | ||
string amountName = resolver?.GetResolvedPropertyName("Amount") ?? "Amount"; | ||
writer.WritePropertyName(amountName); | ||
writer.WriteValue(amount); | ||
} | ||
|
||
private void writeCurrency(CurrencyIsoCode currency, | ||
JsonWriter writer, JsonSerializer serializer, DefaultContractResolver? resolver) | ||
{ | ||
// non-pascal if "weird" resolver | ||
string currencyName = resolver?.GetResolvedPropertyName("Currency") ?? "Currency"; | ||
writer.WritePropertyName(currencyName); | ||
if (_forceStringEnum) | ||
{ | ||
// ignore configured enum value convention, string it is | ||
writer.WriteValue(currency.ToString()); | ||
} | ||
else | ||
{ | ||
// follow configured enum value convention | ||
serializer.Serialize(writer, currency, typeof(CurrencyIsoCode)); | ||
} | ||
} | ||
|
||
#endregion | ||
} |
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,55 @@ | ||
<Project Sdk="Microsoft.NET.Sdk"> | ||
|
||
<PropertyGroup> | ||
<TargetFramework>net6.0</TargetFramework> | ||
<ImplicitUsings>enable</ImplicitUsings> | ||
<Nullable>enable</Nullable> | ||
|
||
<EnforceCodeStyleInBuild>true</EnforceCodeStyleInBuild> | ||
<CodeAnalysisTreatWarningsAsErrors>true</CodeAnalysisTreatWarningsAsErrors> | ||
</PropertyGroup> | ||
|
||
<PropertyGroup> | ||
<Description>Custom serialization/deserialization code samples of monetary quantities with several popular serialization libraries.</Description> | ||
<Product>NMoneys.Serialization</Product> | ||
<Copyright>Copyright © Daniel Gonzalez Garcia 2011</Copyright> | ||
</PropertyGroup> | ||
|
||
<PropertyGroup> | ||
<RepositoryUrl>https://github.com/dgg/nmoneys.git</RepositoryUrl> | ||
<RepositoryType>git</RepositoryType> | ||
<PackageReadmeFile>README.md</PackageReadmeFile> | ||
<PackageTags>.net;dotnet;C#;currency;money;iso;monetary;quantity;iso4217;serialization;Json.NET;BSON;Xml;System.Text.Json;Entity Framework</PackageTags> | ||
</PropertyGroup> | ||
|
||
<PropertyGroup> | ||
<Version>5.0.0</Version> | ||
<AssemblyVersion>5.0.0.0</AssemblyVersion> | ||
<FileVersion>5.0.0.0</FileVersion> | ||
</PropertyGroup> | ||
|
||
<ItemGroup> | ||
<AssemblyAttribute Include="System.CLSCompliantAttribute"> | ||
<_Parameter1>false</_Parameter1> | ||
</AssemblyAttribute> | ||
</ItemGroup> | ||
|
||
<ItemGroup> | ||
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" /> | ||
</ItemGroup> | ||
|
||
<PropertyGroup> | ||
<NoWarn>CA1707,CA1508</NoWarn> | ||
</PropertyGroup> | ||
|
||
<ItemGroup> | ||
<Folder Include="BSON\" /> | ||
<Folder Include="EFCore\" /> | ||
<Folder Include="Xml\" /> | ||
</ItemGroup> | ||
|
||
<ItemGroup> | ||
<ProjectReference Include="..\NMoneys\NMoneys.csproj" /> | ||
</ItemGroup> | ||
|
||
</Project> |
Oops, something went wrong.