How to Handle Numbers Represented as Strings in the Input to a .NET AWS Lambda Function

Download full source code.

If you are using the lambda.EmptyFunction template, deserialization will be performed by Amazon.Lambda.Serialization.SystemTextJson.DefaultLambdaJsonSerializer.

This DefaultLambdaJsonSerializer cannot handle numbers represented as strings.

For example, if you have a Car type which is the input to the function handler, it might look like this -

public class Function
{
    public string FunctionHandler(Car input, ILambdaContext context)
    {
        return input.ToString();
    }
}

public class Car
{
    public string Make { get; set; }
    public string Model { get; set; }
    public int Year { get; set; }
    public override string ToString()
    {
        return $"Your car is a {Make} {Model}, made in {Year}.";
    }
}

If you invoke it with the following payload it will work -

dotnet lambda invoke-function HandlingNumbersAsStringAndEnums --payload '{"make":"Ford","model":"Focus","year":2022}'

Payload:
"Your car is a Ford Focus, made in 2022."

But if you invoke it with this payload it will fail -

dotnet lambda invoke-function HandlingNumbersAsStringAndEnums --payload '{"make":"Ford","model":"Focus","year":"2022"}'

Payload:
{
  "errorType": "JsonSerializerException",
  "errorMessage": "Error converting the Lambda event JSON payload to type HandlingNumbersAsStringsInJson.Car: The JSON value could not be converted to System.Int32. Path: $.year | LineNumber: 0 | BytePositionInLine: 44.",

The difference is that the year property is a string in the second payload. The DefaultLambdaJsonSerializer cannot handle this.

You have two choices to fix this -

  • Create your own implementation of a serializer/deserializer and use that in your Lambda function.
  • Instead of taking a Car as the input to your function handler, take a Stream and deserialize it yourself.

Creating your own implementation of a serializer/deserializer

This is not a difficult task. Use the DefaultLambdaJsonSerializer as a base class and set your serialization options there.

     
using System.Text.Json;
using System.Text.Json.Serialization;
using Amazon.Lambda.Serialization.SystemTextJson;

namespace HandlingNumbersAsStringsInJson;

public class NumbersAsStringsSerializer : DefaultLambdaJsonSerializer
{
    public NumbersAsStringsSerializer()
        : base(ConfigureJsonSerializerOptions) { }

    private static void ConfigureJsonSerializerOptions(JsonSerializerOptions options)
    {
        options.NumberHandling = JsonNumberHandling.AllowReadingFromString;
    }

    // This is a shorter version of the above
    // public NumbersAsStringsSerializer()
    //    : base((options) => options.NumberHandling = JsonNumberHandling.AllowReadingFromString) { }
}

Then in the Function.cs file, change the serializer to the one you just created -

[assembly: LambdaSerializer(typeof(NumbersAsStringsSerializer))]

Now you can invoke the function with the payload that has a string for the year property and it will work.

Taking a Stream as the input to your Lambda function

The second option is to take a stream as the input to your function handler instead of the Car type and perform the deserialization there.

public string FunctionHandler(Stream input, ILambdaContext context)
{
    var options = new JsonSerializerOptions
    {
        NumberHandling = JsonNumberHandling.AllowReadingFromString,
        PropertyNameCaseInsensitive = true
    };

    var car = JsonSerializer.Deserialize<Car>(input, options);
    return car.ToString();
}

For better efficiency you can move var options =... outside the function handler. That way, the code is run only the first time the function is invoked.

In the attached zip file you will see that the DefaultLambdaJsonSerializer is still in place, it will be used to serialize the response back to the caller.

In a follow up I’ll show how to handle enums in the same way.

Download full source code.

comments powered by Disqus

Related