I went down writing a custom solution
It doesnt cover all types, but enough for the ones I use. Good starting ground for anyone to develop further,
from re import compile as re_compile
class Serializer:
re_number = re_compile(r"^-?\d+?\.?\d*$")
def serialize(self, data: any) -> dict:
if isinstance(data, bool): # booleans are a subtype of integers so place above int
return {'BOOL': data}
if isinstance(data, (int, float)):
return {'N': str(data)}
if isinstance(data, type(None)) or not data: # place below int (0) and bool (False)
# returns NULL for empty list, tuple, dict, set or string
return {'NULL': True}
if isinstance(data, (list, tuple)):
return {'L': [self.serialize(v) for v in data]}
if isinstance(data, set):
if all([isinstance(v, str) for v in data]):
return {'SS': data}
if all([self.re_number.match(str(v)) for v in data]):
return {'NS': [str(v) for v in data]}
if isinstance(data, dict):
return {'M': {k: self.serialize(v) for k, v in data.items()}}
return {'S': str(data)} # safety net to catch all others
def deserialize(self, data: dict) -> dict:
_out = {}
if not data:
return _out
for k, v in data.items():
if k in ('S', 'SS', 'BOOL'):
return v
if k == 'N':
return float(v) if '.' in v else int(v)
if k == 'NS':
return [float(_v) if '.' in _v else int(_v) for _v in v]
if k == 'M':
return {_k: self.deserialize(_v) for _k, _v in v.items()}
if k == 'L':
return [self.deserialize(_v) for _v in v]
if k == 'NULL':
return None
_out[k] = self.deserialize(v)
return _out
Usage
serialized = Serializer().serialize(input_dict)
print(serialized)
deserialized = Serializer().deserialize(serialized)
print(deserialized)
DynamoDB (python)
dynamodb = boto3.client('dynamodb')
dynamodb.put_item(
TableName=table_name,
Item={
'id': {'S': id},
'data': Serializer().serialize(data)
}
)
response = dynamodb.get_item(
TableName=table_name,
Key={
'id': {'S': id}
}
)
data = Serializer().deserialize(response['Item'])