-
Notifications
You must be signed in to change notification settings - Fork 860
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Support decimal precision for Document.FromJson and ToJson by using System.Text.Json instead of LitJson #3534
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
{ | ||
"services": [ | ||
{ | ||
"serviceName": "DynamoDBv2", | ||
"type": "patch", | ||
"changeLogMessages": [ | ||
"Switch Document.FromJson and ToJson to use System.Text.Json instead of LitJson. This supports additional precision for decimal values." | ||
] | ||
} | ||
] | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -16,12 +16,9 @@ | |
using System; | ||
using System.Collections.Generic; | ||
using System.Globalization; | ||
using System.Text; | ||
|
||
using Amazon.DynamoDBv2.Model; | ||
using Amazon.Util; | ||
using ThirdParty.Json.LitJson; | ||
using System.IO; | ||
using System.Text; | ||
using System.Text.Json; | ||
|
||
namespace Amazon.DynamoDBv2.DocumentModel | ||
{ | ||
|
@@ -40,11 +37,18 @@ internal static class JsonUtils | |
/// <returns></returns> | ||
public static Document FromJson(string jsonText) | ||
{ | ||
var json = JsonMapper.ToObject(jsonText); | ||
if (!json.IsObject) | ||
var json = JsonDocument.Parse(jsonText, new JsonDocumentOptions | ||
{ | ||
AllowTrailingCommas = true, | ||
CommentHandling = JsonCommentHandling.Skip | ||
}); | ||
|
||
if (json.RootElement.ValueKind != JsonValueKind.Object) | ||
{ | ||
throw new InvalidOperationException("Expected object at JSON root."); | ||
} | ||
|
||
var document = ToEntry(json, DynamoDBEntryConversion.V2) as Document; | ||
var document = ToEntry(json.RootElement, DynamoDBEntryConversion.V2) as Document; | ||
if (document == null) | ||
throw new InvalidOperationException(); | ||
|
||
|
@@ -58,15 +62,15 @@ public static Document FromJson(string jsonText) | |
/// <returns>An <see cref="IEnumerable{T}"/> of type <see cref="Document"/></returns> | ||
public static IEnumerable<Document> FromJsonArray(string jsonText) | ||
{ | ||
var json = JsonMapper.ToObject(jsonText); | ||
if (!json.IsArray) | ||
var json = JsonDocument.Parse(jsonText); | ||
if (json.RootElement.ValueKind != JsonValueKind.Array) | ||
throw new InvalidOperationException("Expected array at JSON root."); | ||
|
||
var array = new List<Document>(); | ||
for(int i=0;i<json.Count;i++) | ||
|
||
foreach (var element in json.RootElement.EnumerateArray()) | ||
{ | ||
var item = json[i]; | ||
var entry = ToEntry(item, DynamoDBEntryConversion.V2) as Document; | ||
var entry = ToEntry(element, DynamoDBEntryConversion.V2) as Document; | ||
if (entry == null) | ||
throw new InvalidOperationException(); | ||
array.Add(entry); | ||
|
@@ -83,19 +87,16 @@ public static IEnumerable<Document> FromJsonArray(string jsonText) | |
/// <returns></returns> | ||
public static string ToJson(Document document, bool prettyPrint) | ||
{ | ||
var sb = new StringBuilder(); | ||
var writer = new JsonWriter(sb); | ||
writer.PrettyPrint = prettyPrint; | ||
using var stream = new MemoryStream(); | ||
using var writer = new Utf8JsonWriter(stream, new JsonWriterOptions | ||
{ | ||
Indented = prettyPrint | ||
}); | ||
|
||
WriteJson(document, writer, DynamoDBEntryConversion.V2); | ||
|
||
// Trim everything before the first '{' character | ||
var jsonIndex = FirstIndex(sb, '{'); | ||
if (jsonIndex > 0) | ||
sb.Remove(0, jsonIndex); | ||
|
||
var jsonText = sb.ToString(); | ||
return jsonText; | ||
writer.Flush(); | ||
return Encoding.UTF8.GetString(stream.ToArray()); | ||
} | ||
|
||
|
||
|
@@ -227,70 +228,79 @@ private static bool TryDecodeBase64(string base64Data, out byte[] bytes) | |
} | ||
} | ||
|
||
// Returns a DynamoDB entry for the given JSON data | ||
private static DynamoDBEntry ToEntry(JsonData data, DynamoDBEntryConversion conversion) | ||
/// <summary> | ||
/// Returns a DynamoDB entry for the given JSON data | ||
/// </summary> | ||
private static DynamoDBEntry ToEntry(JsonElement data, DynamoDBEntryConversion conversion) | ||
{ | ||
if (data == null) | ||
if (data.ValueKind == JsonValueKind.Null) | ||
return new DynamoDBNull(); | ||
|
||
if (data.IsObject) | ||
if (data.ValueKind == JsonValueKind.Object) | ||
{ | ||
var document = new Document(); | ||
foreach (var propertyName in data.PropertyNames) | ||
foreach (var property in data.EnumerateObject()) | ||
{ | ||
var nestedData = data[propertyName]; | ||
var entry = ToEntry(nestedData, conversion); | ||
document[propertyName] = entry; | ||
var entry = ToEntry(property.Value, conversion); | ||
document[property.Name] = entry; | ||
} | ||
return document; | ||
} | ||
|
||
if (data.IsArray) | ||
if (data.ValueKind == JsonValueKind.Array) | ||
{ | ||
var list = new DynamoDBList(); | ||
for(int i=0;i<data.Count;i++) | ||
foreach (var property in data.EnumerateArray()) | ||
{ | ||
var item = data[i]; | ||
var entry = ToEntry(item, conversion); | ||
var entry = ToEntry(property, conversion); | ||
list.Add(entry); | ||
} | ||
return list; | ||
} | ||
|
||
if (data.IsBoolean) | ||
return new UnconvertedDynamoDBEntry((bool)data).Convert(conversion); | ||
|
||
if (data.IsDouble) | ||
return new UnconvertedDynamoDBEntry((double)data).Convert(conversion); | ||
|
||
if (data.IsInt) | ||
return new UnconvertedDynamoDBEntry((int)data).Convert(conversion); | ||
if (data.ValueKind == JsonValueKind.False) | ||
return new UnconvertedDynamoDBEntry(false).Convert(conversion); | ||
|
||
if (data.IsUInt) | ||
return new UnconvertedDynamoDBEntry((uint)data).Convert(conversion); | ||
if (data.ValueKind == JsonValueKind.True) | ||
return new UnconvertedDynamoDBEntry(true).Convert(conversion); | ||
|
||
if (data.IsLong) | ||
return new UnconvertedDynamoDBEntry((long)data).Convert(conversion); | ||
|
||
if (data.IsULong) | ||
return new UnconvertedDynamoDBEntry((ulong)data).Convert(conversion); | ||
if (data.ValueKind == JsonValueKind.Number) | ||
{ | ||
if (data.TryGetDecimal(out decimal decimalValue)) | ||
return new UnconvertedDynamoDBEntry(decimalValue).Convert(conversion); | ||
if (data.TryGetDouble(out double doubleValue)) | ||
return new UnconvertedDynamoDBEntry(doubleValue).Convert(conversion); | ||
if (data.TryGetInt32(out int intValue)) | ||
return new UnconvertedDynamoDBEntry(intValue).Convert(conversion); | ||
if (data.TryGetUInt32(out uint uintValue)) | ||
return new UnconvertedDynamoDBEntry(uintValue).Convert(conversion); | ||
if (data.TryGetInt64(out long int64Value)) | ||
return new UnconvertedDynamoDBEntry(int64Value).Convert(conversion); | ||
if (data.TryGetUInt64(out ulong uint64Value)) | ||
return new UnconvertedDynamoDBEntry(uint64Value).Convert(conversion); | ||
|
||
// This preserves existing fallback behavior we had with LitJson | ||
return new UnconvertedDynamoDBEntry(default(long)).Convert(conversion); | ||
} | ||
|
||
if (data.IsString) | ||
return new UnconvertedDynamoDBEntry((string)data).Convert(conversion); | ||
if (data.ValueKind == JsonValueKind.String) | ||
return new UnconvertedDynamoDBEntry(data.ToString()).Convert(conversion); | ||
|
||
throw new InvalidOperationException(string.Format(CultureInfo.InvariantCulture, | ||
"Unable to convert JSON data of type {0} to DynamoDB type.", data.GetJsonType())); | ||
"Unable to convert JSON data of type {0} with value {1} to DynamoDB type.", data.ValueKind, data.GetRawText())); | ||
} | ||
|
||
// Writes a JSON representation of the given DynamoDBEntry | ||
internal static void WriteJson(DynamoDBEntry entry, JsonWriter writer, DynamoDBEntryConversion conversion) | ||
/// <summary> | ||
/// Writes a JSON representation of the given DynamoDBEntry | ||
/// </summary> | ||
internal static void WriteJson(DynamoDBEntry entry, Utf8JsonWriter writer, DynamoDBEntryConversion conversion) | ||
{ | ||
entry = entry.ToConvertedEntry(conversion); | ||
|
||
var document = entry as Document; | ||
if (document != null) | ||
{ | ||
writer.WriteObjectStart(); | ||
writer.WriteStartObject(); | ||
|
||
// Both item attributes and entries in M type are unordered, so sorting by key | ||
// http://docs.aws.amazon.com/amazondynamodb/latest/developerguide/DataModel.html#DataModel.DataTypes | ||
|
@@ -302,7 +312,7 @@ internal static void WriteJson(DynamoDBEntry entry, JsonWriter writer, DynamoDBE | |
writer.WritePropertyName(name); | ||
WriteJson(value, writer, conversion); | ||
} | ||
writer.WriteObjectEnd(); | ||
writer.WriteEndObject(); | ||
return; | ||
} | ||
|
||
|
@@ -320,71 +330,81 @@ internal static void WriteJson(DynamoDBEntry entry, JsonWriter writer, DynamoDBE | |
{ | ||
var itemType = primitiveList.Type; | ||
|
||
writer.WriteArrayStart(); | ||
writer.WriteStartArray(); | ||
foreach (var item in primitiveList.Entries) | ||
{ | ||
var itemValue = item.Value; | ||
WritePrimitive(writer, itemType, itemValue); | ||
} | ||
writer.WriteArrayEnd(); | ||
writer.WriteEndArray(); | ||
return; | ||
} | ||
|
||
var ddbList = entry as DynamoDBList; | ||
if (ddbList != null) | ||
{ | ||
writer.WriteArrayStart(); | ||
foreach(var item in ddbList.Entries) | ||
writer.WriteStartArray(); | ||
foreach (var item in ddbList.Entries) | ||
{ | ||
WriteJson(item, writer, conversion); | ||
} | ||
writer.WriteArrayEnd(); | ||
writer.WriteEndArray(); | ||
return; | ||
} | ||
|
||
var ddbBool = entry as DynamoDBBool; | ||
if (ddbBool != null) | ||
{ | ||
writer.Write(ddbBool.Value); | ||
writer.WriteBooleanValue(ddbBool.Value); | ||
return; | ||
} | ||
|
||
var ddbNull = entry as DynamoDBNull; | ||
if (ddbNull != null) | ||
{ | ||
writer.Write((string)null); | ||
writer.WriteNullValue(); | ||
return; | ||
} | ||
|
||
throw new InvalidOperationException(string.Format(CultureInfo.InvariantCulture, | ||
"Unable to convert entry of type {0} to JSON", entry.GetType().FullName)); | ||
} | ||
|
||
// Write the contents of a Primitive object as JSON data | ||
private static void WritePrimitive(JsonWriter writer, DynamoDBEntryType type, object value) | ||
/// <summary> | ||
/// Write the contents of a Primitive object as JSON data | ||
/// </summary> | ||
private static void WritePrimitive(Utf8JsonWriter writer, DynamoDBEntryType type, object value) | ||
{ | ||
var stringValue = value as string; | ||
|
||
switch (type) | ||
{ | ||
case DynamoDBEntryType.Numeric: | ||
writer.WriteRaw(stringValue); | ||
break; | ||
{ | ||
#if NETCOREAPP3_1 // WriteRawValue was added in .NET 6, but we need to write out Number values without quotes | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Question: Does this mean for the .NET Framework target we'd try to use There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It's available in 4.7.2. At the time 3.1 was the only target we needed this workaround for. |
||
using var document = JsonDocument.Parse(stringValue); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think there is an alternative, simpler solution here which is to use something like:
This way, we don't need to have a preprocessor directive. But maybe I'm missing something and there is a reason why it was done this way.😅 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is this what the user was complaining about for losing decimal precision? If that's the case then nvm! There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We would potentially lose precision trying to convert the string version of the number to the best fit .NET numeric type. Overall I don't think that will simplify the logic. |
||
document.WriteTo(writer); | ||
#else | ||
writer.WriteRawValue(stringValue); | ||
#endif | ||
break; | ||
} | ||
case DynamoDBEntryType.String: | ||
writer.Write(stringValue); | ||
writer.WriteStringValue(stringValue); | ||
break; | ||
case DynamoDBEntryType.Binary: | ||
var bytes = value as byte[]; | ||
var base64 = Convert.ToBase64String(bytes); | ||
writer.Write(base64); | ||
writer.WriteBase64StringValue(bytes); | ||
break; | ||
default: | ||
throw new InvalidOperationException(string.Format(CultureInfo.InvariantCulture, | ||
"Unsupport DynamoDBEntryType: {0}", type)); | ||
} | ||
} | ||
|
||
// Finds first instance of a character | ||
/// <summary> | ||
/// Finds first instance of a character | ||
/// </summary> | ||
private static int FirstIndex(StringBuilder sb, char toMatch) | ||
{ | ||
for(int i=0;i<sb.Length;i++) | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In your description you say
"A slight change in behavior is the JSON being past into the FromJson is more strictly parsed. Meaning it doesn't allow extra , for the last property in an object. LitJson would allow that. Given that isn't valid JSON this seems an acceptable change for V4."
but here you are allowing trailing commas. Did you set
AllowTrailingCommas
to true on purpose to keep the same behavior as litJson or did you mean to set this tofalse
?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh yeah. I copied the comment from Alex's original PR he must have added setting that property afterwards. I'll remove that comment from the PR description.