marshmallowで相互排他的なfieldを定義する方法

はじめに

こういうJSONを許したい。

{
  "left": {
    "name": "foo",
    "value": 100,
  }
}

あるいはこう。

{
  "right": {
    "name": "foo",
    "value": 100.0,
  }
}

left,rightというfieldの内どちらか1つだけ値が入るデータをvalidとしたい。例えば以下はダメ。

{}

これもだめ。

{
  "left": {
    "name": "foo",
    "value": 100,
  },
  "right": {
    "name": "foo",
    "value": 100.0,
  }
}

2つのleft,rightというfieldの構造がテキトウ過ぎるけれど。2つ(Nつ)あるfieldの内1つだけに値が入るという状態にしたい。

方法

field自体にこのような機能をつけるのは無理で。validate_schemasというschemaレベルのvalidationを使う。

import marshmallow as ma


class Item(ma.Schema):
    name = ma.fields.String(required=True)
    value = ma.fields.Integer(required=True)


class Item2(ma.Schema):
    name = ma.fields.String(required=True)
    value = ma.fields.Number(required=True)


class S(ma.Schema):
    left = ma.fields.Nested(Item)
    right = ma.fields.Nested(Item2)

    @ma.validates_schema
    def mutual(self, data):
        items = [item for item in [data.get("left"), data.get("right")] if item]
        if len(items) != 1:
            raise ma.ValidationError("items0 or items1")


print(S().load({}))
print(S().load({"left": {"name": "foo", "value": 10}}))
print(S().load({"right": {"name": "foo", "value": 10}}))
print(S().load({"left": {"name": "foo", "value": 10}, "right": {"name": "foo", "value": 10}}))

# UnmarshalResult(data={}, errors={'_schema': ['items0 or items1']})
# UnmarshalResult(data={'left': {'value': 10, 'name': 'foo'}}, errors={})
# UnmarshalResult(data={'right': {'value': 10.0, 'name': 'foo'}}, errors={})
# UnmarshalResult(data={'right': {'value': 10.0, 'name': 'foo'}, 'left': {'value': 10, 'name': 'foo'}}, errors={'_schema': ['items0 or items1']})