我如何构建我的 JSON 模式来验证 DynamoDB 和 REST API?

How can I structure my JSON schema to validate for DynamoDB and RESTAPI?

我正在编写一个 REST API,它将几个复杂的对象存储到 AWS DynamoDB,然后在请求时检索它们,对它们执行计算,并 return 结果。这是一大段提取、简化、重命名的伪代码。

class Widget:
    def __init__(self, height, weight):
        self.height = height
        self.weight = weight

class Machine:
    def __init__ (self, widgets):
        self.widgets = widgets
    def useful_method ():
        return "something great"

class WidgetSchema (Schema):
    height = fields.Decimal()
    weight = fields.Decimal()
    @post_load
    def make_widget (self, data):
        return Widget(*data)

class MachineSchema (Schema):
    widgets = fields.List(fields.Nested(WidgetSchema))
    def make_machine (self, data):
        return Machine(*data)

app = Flask(__name__)
dynamodb = boto3.resource("dynamodb", ...) 

@app.route("/machine/<uuid:machine_id>", methods=['POST'])
def create_machine(machine_id):
    input_json = request.get_json()
    validated_input = MachineSchema().load(input_json)
    # NOTE: validated_input should be a Python dict which
    # contains Decimals instead of floats, for storage in DynamoDB.
    validate_input['id'] = machine_id
    dynamodb.Table('machine').put_item(Item=validate_input)
    return jsonify({"status", "success", error_message = ""})

@app.route("/machine/<uuid:machine_id>/compute", methods=['GET'])
def get_machine(machine_id):
    result = dynamodb.Table('machine').get_item(Key=machine_id)
    return jsonify(result['Item'])

@app.route("/machine/<uuid:machine_id>/compute", methods=['GET'])
def compute_machine(machine_id):
    result = dynamodb.Table('machine').get_item(Key=machine_id)
    validated_input = MachineSchema().load(result['Item'])
    # NOTE: validated_input should be a Machine object
    # which has made use of the post_load
    return jsonify(validated_input.useful_method())

这个问题是我需要让我的 Marshmallow 架构承担双重职责。对于初学者,在 create_machine 函数中,我需要模式来确保调用我的 REST API 的用户向我传递了一个格式正确的对象,没有额外的字段并满足所有必填字段等。我需要以确保我最终没有在数据库中存储无效垃圾。它还需要递归地抓取输入 JSON 并将所有 JSON 值转换为正确的类型。例如,Dynamo 不支持浮点数,因此它们必须是 Decimals,如此处所示。这是 Marshmallow 制作起来非常简单的东西。如果没有 post_load,这正是 validated_input.

的结果

模式的第二个工作是它需要它获取从 DynamoDB 检索的 Python 对象,它看起来 几乎 与用户输入完全一样 JSON 除浮点数外均为小数,并将其转换为我的 Python 对象、Machine 和 Widget。这是我需要再次读取对象的地方,但这次使用 post 加载来创建对象。然而,在这种情况下,我不希望我的数字是小数。我希望它们是标准的 Python 花车。

我可以为此编写两个完全不同的 Marshmallow 模式并完成它,很明显。一个是身高和体重的小数,另一个是浮点数。一个人会为每个对象加载 post,而另一个人会 none。但是编写两个相同的模式是一个巨大的痛苦。我的模式定义有几百行长。继承具有 post 负载的数据库版本似乎不是正确的方向,因为我需要更改任何 fields.Nested 以指向正确的 class。例如,即使我从 MachineSchema 继承了 MachineSchemaDBVersion,并添加了一个 post_load,MachineScehemaDBVersion 仍然会引用 WidgetScehema,而不是 WidgetSchema 的某个数据库版本,除非我也覆盖了 widgets 字段。

我可能会派生出我自己的 Schema 对象并传递一个标志,以判断我们是否处于 DB 模式。

人们通常如何处理希望将 REST API 输入或多或少直接存储到 DynamoDB 并进行一些验证,然后使用该数据构建 Python 对象进行计算的问题?

我尝试过的一种方法是让我的模式始终实例化我的 Python 对象,然后使用来自完全构造的对象的转储将它们复制到数据库中。问题在于计算库的对象(在我的示例 Machine 或 Widget 中)没有我需要存储在数据库中的所有必填字段,例如 ID、名称或描述。这些对象专门用于计算。

我最终找到了解决方案。实际上,我所做的是生成 Marshmallow 模式,专门用于从 DynamoDB 转换为 Python 对象。所有 Schema 类 都有 @post_load 方法转换为 Python 对象,所有字段都标有它们在 Python 世界而不是数据库世界中需要的类型.

在验证来自 REST API 的输入并确保不允许任何错误数据进入数据库时​​,我调用 MySchema().validate(input_json),检查是否没有错误,如果不,将 input_json 转储到数据库中。

这只留下一个额外的问题,那就是 input_json 需要清理才能进入数据库,这是我之前使用 Marshmallow 所做的。但是,这也可以通过调整我的 JSON 解码器以从浮点数读取小数来轻松完成。

总而言之,我的 JSON 解码器正在执行递归遍历数据结构并将 Float 转换为 Decimal 的工作,与 Marshmallow 分开。 Marshmallow 运行 对每个对象的字段进行验证,但仅检查结果是否有错误。然后将原始输入转储到数据库中。

我需要添加这一行来转换为十进制。

app.json_decoder = partial(flask.json.JSONDecoder, parse_float=decimal.Decimal)

我的创建函数现在看起来像这样。请注意原始 input_json(由我更新的 JSON 解码器解析)是如何直接插入数据库的,而不是 Marshmallow 输出的任何数据。

@app.route("/machine/<uuid:machine_id>", methods=['POST'])
def create_machine(machine_id):
    input_json = request.get_json() # Already ready to be DB input as is.
    errors = MachineSchema().validate(input_json)
    if errors:
      return jsonify({"status": "failure",message = dumps(errors)})
    else:
      input_json['id'] = machine_id
      dynamodb.Table('machine').put_item(Item=input_json)
      return jsonify({"status", "success", error_message = ""})