Skip to content

Commit

Permalink
untested json_net and test_json serializations
Browse files Browse the repository at this point in the history
  • Loading branch information
dgg committed Jan 12, 2024
1 parent 6b51e2d commit 55a056b
Show file tree
Hide file tree
Showing 13 changed files with 697 additions and 1 deletion.
14 changes: 14 additions & 0 deletions NMoneys.sln
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "archive", "archive", "{DB66
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NMoneys.Exchange", "archive\NMoneys.Exchange\NMoneys.Exchange.csproj", "{97C58515-92D8-4ECF-86B6-CFAABD66279D}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NMoneys.Serialization", "src\NMoneys.Serialization\NMoneys.Serialization.csproj", "{F61AA4C5-EAF0-425F-BF23-8B04DB85A1FF}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NMoneys.Serialization.Tests", "tests\NMoneys.Serialization.Tests\NMoneys.Serialization.Tests.csproj", "{A28BEBB8-EC70-4FC5-AFE7-9989F816347B}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand All @@ -36,10 +40,20 @@ Global
{97C58515-92D8-4ECF-86B6-CFAABD66279D}.Debug|Any CPU.Build.0 = Debug|Any CPU
{97C58515-92D8-4ECF-86B6-CFAABD66279D}.Release|Any CPU.ActiveCfg = Release|Any CPU
{97C58515-92D8-4ECF-86B6-CFAABD66279D}.Release|Any CPU.Build.0 = Release|Any CPU
{F61AA4C5-EAF0-425F-BF23-8B04DB85A1FF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{F61AA4C5-EAF0-425F-BF23-8B04DB85A1FF}.Debug|Any CPU.Build.0 = Debug|Any CPU
{F61AA4C5-EAF0-425F-BF23-8B04DB85A1FF}.Release|Any CPU.ActiveCfg = Release|Any CPU
{F61AA4C5-EAF0-425F-BF23-8B04DB85A1FF}.Release|Any CPU.Build.0 = Release|Any CPU
{A28BEBB8-EC70-4FC5-AFE7-9989F816347B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{A28BEBB8-EC70-4FC5-AFE7-9989F816347B}.Debug|Any CPU.Build.0 = Debug|Any CPU
{A28BEBB8-EC70-4FC5-AFE7-9989F816347B}.Release|Any CPU.ActiveCfg = Release|Any CPU
{A28BEBB8-EC70-4FC5-AFE7-9989F816347B}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
{0484566C-1D24-433E-90F2-B8B44BEF2FB2} = {558BFE88-EB25-4BFA-91B8-4CFF8696CED4}
{891B60F6-888D-488A-89D0-6E25FE1A7883} = {6454722E-5710-44B0-8EDE-1DE92929E641}
{97C58515-92D8-4ECF-86B6-CFAABD66279D} = {DB66F4E1-075E-4E1C-931E-5DD8F956E484}
{F61AA4C5-EAF0-425F-BF23-8B04DB85A1FF} = {6454722E-5710-44B0-8EDE-1DE92929E641}
{A28BEBB8-EC70-4FC5-AFE7-9989F816347B} = {558BFE88-EB25-4BFA-91B8-4CFF8696CED4}
EndGlobalSection
EndGlobal
190 changes: 190 additions & 0 deletions src/NMoneys.Serialization/Json_NET/DescriptiveMoneyConverter.cs
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
}
119 changes: 119 additions & 0 deletions src/NMoneys.Serialization/Json_NET/MoneyConverter.cs
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
}
55 changes: 55 additions & 0 deletions src/NMoneys.Serialization/NMoneys.Serialization.csproj
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>

0 comments on commit 55a056b

Please sign in to comment.