JSON Serialization
Starting with v0.8.2, LambdaSharp uses System.Text.Json v5.0 instead of Newtonsoft.Json for JSON serialization of built-in types. Custom types are handled with the JSON serializer specified using the LambdaSerializer
assembly attribute.
This article describes how to switch from the default Newtonsoft.Json to System.Text.Json, as well as what to look out for.
Migrating JSON Serialization from Newtonsoft.Json to System.Text.Json
Lambda functions using System.Text.Json must declare LambdaSystemTextJsonSerializer
as their JSON serializer using the JsonSerializer
assembly attribute.
[assembly: Amazon.Lambda.Core.LambdaSerializer(typeof(LambdaSharp.Serialization.LambdaSystemTextJsonSerializer))]
Microsoft has published an excellent migration guide for switching from Newtonsoft.Json to System.Text.Json. In addition to the guide, the following sections explain how to migrate existing data structures.
Alternatively, functions can continue to use Newtonsoft.Json as their JSON serializer by including the LambdaSharp.Serialization.NewtonsoftJson
assembly from NuGet:
[assembly: Amazon.Lambda.Core.LambdaSerializer(typeof(LambdaSharp.Serialization.LambdaNewtonsoftJsonSerializer))]
Update Projects
Upgrade projects to .NET Core 3.1 by changing the target framework in the .csproj file.
- Before:
<TargetFramework>netcoreapp2.1</TargetFramework>
- After:
<TargetFramework>netcoreapp3.1</TargetFramework>
Remove all Newtonsoft.Json package dependencies (version may vary).
- Remove:
<PackageReference Include="Newtonsoft.Json" />
Replace Non-Public Properties/Fields with Public Properties/Fields
Unlike Newtonsoft.Json, System.Text.Json does not serialize non-public properties/fields.
Non-public properties must be converted to public, mutable properties.
- Before:
internal string Name { get; set; }
- After:
public string Name { get; set; }
Limited mutable properties must be converted to public, mutable properties to be deserialized properly.
- Before:
public string Name { get; protected set; }
- After:
public string Name { get; set; }
Non-public fields must be converted to public, mutable fields.
- Before:
internal string Name;
- After:
public string Name;
Convert JSON string values to enum
properties
Newtonsoft.Json provides StringEnumConverter
to convert JSON string to enum
properties. System.Text.Json includes an equivalent converter called JsonStringEnumConverter
in the System.Text.Json.Serialization namespace.
- Before:
[JsonConverter(typeof(StringEnumConverter))]
- After:
[JsonConverter(typeof(JsonStringEnumConverter))]
Convert JSON integer values to DateTimeOffset
/DateTime
properties
Newtonsoft.Json provides UnixDateTimeConverter
to convert JSON integer to DateTime
properties. System.Text.Json does not include such a converter. Instead, LambdaSharp.Serialization defines JsonEpochSecondsDateTimeOffsetConverter
and JsonEpochSecondsDateTimeConverter
to convert DateTimeOffset
and DateTime
, respectively to a JSON integer representing the UNIX epoch in seconds.
[JsonConverter(typeof(JsonEpochSecondsDateTimeOffsetConverter))]
public DateTimeOffset Epoch { get; set; }
--OR--
[JsonConverter(typeof(JsonEpochSecondsDateTimeConverter))]
public DateTime Epoch { get; set; }
Update Property Attributes
Replace attribute for explicitly naming JSON elements.
- Before:
[JsonProperty("name")]
- After:
[JsonPropertyName("name")]
- Requires:
using System.Text.Json.Serialization;
Replace attribute for requiring a JSON property (used by JSON schema generator for API Gateway models)
- Before:
[JsonRequired]
-or-[JsonProperty(Required = Required.DisallowNull)]
- After:
[Required]
- Requires:
using System.ComponentModel.DataAnnotations;
Case-Sensitive Serialization
Newtonsoft.Json is not case-sensitive on property/field names, but System.Text.Json is.
Solution 1: Use Newtonsoft.Json Serializer
Keep using the Newtonsoft.Json serializer instead by adding the LambdaSharp.Serialization.NewtonsoftJson
NuGet package and assembly attribute for it.
[assembly: Amazon.Lambda.Core.LambdaSerializer(typeof(LambdaSharp.Serialization.LambdaNewtonsoftJsonSerializer))]`
Solution 2: Provide Proper Case-Sensitive Spelling for Property/Field
Use the [JsonPropertyName("name")]
attribute to provide the property/field name with the case-sensitive spelling.
class MyClass {
//--- Properties ---
[JsonPropertyName("name")]
public string Name { get; set; }
}
Solution 3: Custom System.Text.Json Serializer Settings
Create a custom serializer that overrides the default System.Text.Json behavior in its constructor.
[assembly: Amazon.Lambda.Core.LambdaSerializer(typeof(MySerializer))]
public class MySerializer : LambdaSharp.Serialization.LambdaSystemTextJsonSerializer {
//--- Constructors ---
public MySerializer() : base(settings => {
settings.settings.PropertyNameCaseInsensitive = true;
}) { }
}
Derived Classes Serialization
Beware of derived classes during serialization. System.Text.Json will only serialize properties of the declared type, not all the properties of the actual instance, unless you use object
as type.
- Before:
LambdaSerializer.Serialize<Car>(new Sedan { ... })
(onlyCar
properties are serialized; any additionalSedan
properties are skipped) - After:
LambdaSerializer.Serialize<object>(mySedan)
(all public properties are serialized)
Polymorphic Serialization
Additional care is required when serializing an abstract syntax tree where nodes share an abstract base definition.
For example, consider the definition for nestable lists with values. Using Newtonsoft.Json
, we may have written something as follows, where ListConverter
and ValueConverter
are JSON-converters that implement serialization for their respective types.
public class AExpression { }
[JsonConverter(ListConverter)]
public class List : AExpression {
public List<AExpression> Items { get; set; } = new List<AExpression>()
}
[JsonConverter(LiteralConverter)]
public class Literal : AExpression {
public string Value { get; set; }
}
Surprisingly, the following expression serializes as [{}]
because the declared type for List
is AExpression
which has no properties!
JsonSerialize.Serialize(new List {
Items = {
new Literal {
Value = 123
}
}
})
The solution is to provide a JsonConverter
for AExpression
that knows how to perform polymorphic serialization based on the actual derived type.
public class ExpressionConverter : JsonConverter<AExpression> {
//--- Class Fields ---
private readonly ListConverter _listSerializer = new ListConverter();
private readonly LiteralConverter _literalSerializer = new LiteralConverter();
//--- Methods ---
public override ACloudFormationExpression Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) {
// Read() implementation omitted for brevity
throw new NotImplementedException();
}
public override void Write(Utf8JsonWriter writer, ACloudFormationExpression value, JsonSerializerOptions options) {
switch(value) {
case List list:
_listSerializer.Write(writer, list, options);
break;
case Literal literal:
_literalSerializer.Write(writer, literal, options);
break;
default:
throw new ArgumentException($"unsupported serialization type {value?.GetType().FullName ?? "<null>"}");
}
}
}
Custom JSON Serializer
Custom JSON serializer implementation can also be used by providing a class that derives from ILambdaJsonSerializer
.
For example, the following JSON serializer uses LitJSON instead.
[assembly: Amazon.Lambda.Core.LambdaSerializer(typeof(MySerializer))]
public class MySerializer : ILambdaJsonSerializer {
//--- Methods ---
public object Deserialize(Stream stream, Type type) {
var reader = new StreamReader(stream);
var json = reader.ReadToEnd();
return LitJson.JsonMapper.ToObject(json, type);
}
public T Deserialize<T>(Stream requestStream) => (T)Deserialize(stream, typeof(T));
public void Serialize<T>(T response, Stream responseStream) {
var json = LitJson.JsonMapper.ToJson(response);
responseStream.Write(Encoding.UTF8.GetBytes(json));
}
}