[AWS Lambda Powertools Python]API GatewayレスポンスのJSONシリアライズエラーに対応する

# 概要

[AWS Lambda Powertools Python](https://awslabs.github.io/aws-lambda-powertools-python/latest/) [^1] を使うとAPI Gatewayのリソースマッピングが直感的に記述できます。
また、dictなりlistなりをreturnしておけば良い感じのJSONテキストにしてくれる等、とても便利です。

[^1]: [AWS Lambda Powertools Python](https://awslabs.github.io/aws-lambda-powertools-python/latest/)

このレスポンスをJSONに変換してくれる部分で少し困ったので、対応した内容を記録します。

# 準備

AWS Lambda Powertools をインストールします

```
$ pip install aws-lambda-powertools
```


# サンプルハンドラーコード

リクエスト `GET /info` に対して、`name` と `created` 要素を返すシンプルな内容です。
問題点に特化したサンプルですが、リクエストと処理が一目でわかる仕組みになっています。


```handler.py
from datetime import datetime
from aws_lambda_powertools.event_handler import APIGatewayRestResolver

app = APIGatewayRestResolver()

def lambda_handler(event, context):
    return app.resolve(event, context)

@app.get('/info')
def get_info():
    return {
        'name': 'myname',
        'created': datetime.now()
    }
```

# テスト用のコード

Contextパラメータを作る部分がちょっと面倒に見えますが、API Gatewayのリクエスト最小eventパラメータで上記のサンプルコードを実行し、レスポンスを print する内容です。
デプロイなしで手元で確認できます。

```test.py
from handler import lambda_handler
from dataclasses import dataclass

@dataclass
class Context:
    function_name: str = 'handler.main'
    memory_limit_in_mb: int = 128
    invoked_function_arn: str = 'arn:aws:lambda:ap-northeast-1:123456789012:function:myFunction'
    aws_request_id: str = '11111111-2222-3333-eeee-ffffffffffff'


event = {
    'path': '/info',
    'httpMethod': 'GET'
}
print(lambda_handler(event, Context()))
```

# 実行結果

datetimeはJSONシリアライズできないので例外が発生します。

```TypeError: Object of type datetime is not JSON serializable```


```bash:エラーの様子
$ python test.py
Traceback (most recent call last):
  File "/home/shinsaka/ws/awspowertools/test.py", line 16, in <module>
    print(lambda_handler(event, Context()))
  File "/home/shinsaka/ws/awspowertools/handler.py", line 7, in lambda_handler
    return app.resolve(event, context)
  File "/home/shinsaka/ws/awspowertools/.venv/lib/python3.9/site-packages/aws_lambda_powertools/event_handler/api_gateway.py", line 509, in resolve
    return self._resolve().build(self.current_event, self._cors)
  File "/home/shinsaka/ws/awspowertools/.venv/lib/python3.9/site-packages/aws_lambda_powertools/event_handler/api_gateway.py", line 571, in _resolve
    return self._call_route(route, match_results.groupdict())  # pass fn args
  File "/home/shinsaka/ws/awspowertools/.venv/lib/python3.9/site-packages/aws_lambda_powertools/event_handler/api_gateway.py", line 625, in _call_route
    return ResponseBuilder(self._to_response(route.func(**args)), route)
  File "/home/shinsaka/ws/awspowertools/.venv/lib/python3.9/site-packages/aws_lambda_powertools/event_handler/api_gateway.py", line 701, in _to_response
    body=self._json_dump(result),
  File "/home/shinsaka/ws/awspowertools/.venv/lib/python3.9/site-packages/aws_lambda_powertools/event_handler/api_gateway.py", line 705, in _json_dump
    return self._serializer(obj)
  File "/home/shinsaka/.pyenv/versions/3.9.12/lib/python3.9/json/__init__.py", line 234, in dumps
    return cls(
  File "/home/shinsaka/.pyenv/versions/3.9.12/lib/python3.9/json/encoder.py", line 199, in encode
    chunks = self.iterencode(o, _one_shot=True)
  File "/home/shinsaka/.pyenv/versions/3.9.12/lib/python3.9/json/encoder.py", line 257, in iterencode
    return _iterencode(o, 0)
  File "/home/shinsaka/ws/awspowertools/.venv/lib/python3.9/site-packages/aws_lambda_powertools/shared/json_encoder.py", line 16, in default
    return super().default(obj)
  File "/home/shinsaka/.pyenv/versions/3.9.12/lib/python3.9/json/encoder.py", line 179, in default
    raise TypeError(f'Object of type {o.__class__.__name__} '
TypeError: Object of type datetime is not JSON serializable
```

# 修正

## カスタムシリアライザーを実装
`date` または `datetime` については `isoformat()` メソッドの結果を返すように実装しました。
（参考: [json.JSONEncoder](https://docs.python.org/ja/3/library/json.html#json.JSONEncoder) [^2]）

[^2]: [json.JSONEncoder](https://docs.python.org/ja/3/library/json.html#json.JSONEncoder)


```utils.py
import json

class CustomEncoder(json.JSONEncoder):
    def default(self, obj):
        from datetime import date, datetime
        if isinstance(obj, (date, datetime)):
            return obj.isoformat()
        return json.JSONEncoder.default(self, obj)

def custom_serializer(obj) -> str:
    return json.dumps(obj, cls=CustomEncoder)
```

## ハンドラーコードを修正

カスタムシリアライザーをimportして `APIGatewayRestResolver` のserializerパラメータへ指定します。

```handler.py
from datetime import datetime
from aws_lambda_powertools.event_handler import APIGatewayRestResolver
from utils import custom_serializer  # 追記

app = APIGatewayRestResolver(serializer=custom_serializer)  # serializerを指定

def lambda_handler(event, context):
    return app.resolve(event, context)

@app.get('/info')
def get_info():
    return {
        'name': 'myname',
        'created': datetime.now()
    }
```

## 修正後の実行結果

問題のdatetimeオブジェクトは isoformat() 結果の文字列となり、正常にレスポンスできました。


```bash:成功しました
$ python test.py
{'statusCode': 200, 'headers': {'Content-Type': 'application/json'}, 'body': '{"name": "myname", "created": "2022-09-15T13:24:40.348289"}', 'isBase64Encoded': False}
```