class ListUnionSchema(marshmallow.Schema): """Schema with a list of unions.""" l = marshmallow.fields.List( marshmallow_union.Union( [marshmallow.fields.Int(), marshmallow.fields.String()]))
def test_union(self): import marshmallow_union self.assertFieldsEqual( field_for_schema(Union[int, str]), marshmallow_union.Union(fields=[fields.Integer(), fields.String()], required=True), )
class MappingSchema(marshmallow.Schema): """Schema with union inside mapping.""" items = marshmallow.fields.Dict( marshmallow.fields.String(), marshmallow_union.Union([ marshmallow.fields.Integer(), marshmallow.fields.List(marshmallow.fields.Integer()), ]), )
class PersonSchema(marshmallow.Schema): """Schema with reverse candidates.""" name = marshmallow.fields.String() number_or_numbers = marshmallow_union.Union( [ marshmallow.fields.List(marshmallow.fields.Integer()), marshmallow.fields.Integer(), ], reverse_serialize_candidates=True, )
class OtherSchema(marshmallow.Schema): """Schema with forward candidates.""" name = marshmallow.fields.String() number_or_numbers = marshmallow_union.Union( [ marshmallow.fields.List(marshmallow.fields.Integer()), marshmallow.fields.Integer(), ], reverse_serialize_candidates=False, )
def _get_schema_for_field(self, obj, field): """Patch marshmallow_jsonschema.base.JSONSchema to support marshmallow-dataclass[union].""" if isinstance(field, fields.Raw) and field.allow_none and not field.validate: # raw fields shouldn't be type string but type any. bug in marshmallow_dataclass:__init__.py: # if typ is Any: # metadata.setdefault("allow_none", True) # return marshmallow.fields.Raw(**metadata) return {"type": ["string", "number", "object", "array", "boolean", "null"]} if isinstance(field, marshmallow_dataclass.union_field.Union): # convert to marshmallow_union.Union field = marshmallow_union.Union([subfield for _, subfield in field.union_fields], metadata=field.metadata, required=field.required, name=field.name, parent=field.parent, root=field.root, error_messages=field.error_messages, default_error_messages=field.default_error_messages, default=field.default, allow_none=field.allow_none) return super()._get_schema_for_field(obj, field)
def field_for_schema( typ: type, default=marshmallow.missing, metadata: Mapping[str, Any] = None, base_schema: Optional[Type[marshmallow.Schema]] = None, ) -> marshmallow.fields.Field: """ Get a marshmallow Field corresponding to the given python type. The metadata of the dataclass field is used as arguments to the marshmallow Field. :param typ: The type for which a field should be generated :param default: value to use for (de)serialization when the field is missing :param metadata: Additional parameters to pass to the marshmallow field constructor :param base_schema: marshmallow schema used as a base class when deriving dataclass schema >>> int_field = field_for_schema(int, default=9, metadata=dict(required=True)) >>> int_field.__class__ <class 'marshmallow.fields.Integer'> >>> int_field.default 9 >>> field_for_schema(str, metadata={"marshmallow_field": marshmallow.fields.Url()}).__class__ <class 'marshmallow.fields.Url'> """ metadata = {} if metadata is None else dict(metadata) if default is not marshmallow.missing: metadata.setdefault("default", default) # 'missing' must not be set for required fields. if not metadata.get("required"): metadata.setdefault("missing", default) else: metadata.setdefault("required", True) # If the field was already defined by the user predefined_field = metadata.get("marshmallow_field") if predefined_field: return predefined_field # Generic types specified without type arguments if typ is list: typ = List[Any] elif typ is dict: typ = Dict[Any, Any] # Base types field = _field_by_type(typ, base_schema) if field: return field(**metadata) # Generic types origin = typing_inspect.get_origin(typ) if origin: arguments = typing_inspect.get_args(typ, True) if origin in (list, List): child_type = field_for_schema(arguments[0], base_schema=base_schema) return marshmallow.fields.List(child_type, **metadata) if origin in (tuple, Tuple): children = tuple( field_for_schema(arg, base_schema=base_schema) for arg in arguments) return marshmallow.fields.Tuple(children, **metadata) elif origin in (dict, Dict): return marshmallow.fields.Dict( keys=field_for_schema(arguments[0], base_schema=base_schema), values=field_for_schema(arguments[1], base_schema=base_schema), **metadata, ) elif typing_inspect.is_optional_type(typ): subtyp = next(t for t in arguments if t is not NoneType) # type: ignore # Treat optional types as types with a None default metadata["default"] = metadata.get("default", None) metadata["missing"] = metadata.get("missing", None) metadata["required"] = False return field_for_schema(subtyp, metadata=metadata, base_schema=base_schema) elif typing_inspect.is_union_type(typ): subfields = [ field_for_schema(subtyp, metadata=metadata, base_schema=base_schema) for subtyp in arguments ] import marshmallow_union return marshmallow_union.Union(subfields, **metadata) # typing.NewType returns a function with a __supertype__ attribute newtype_supertype = getattr(typ, "__supertype__", None) if newtype_supertype and inspect.isfunction(typ): # Add the information coming our custom NewType implementation metadata = { "description": typ.__name__, **getattr(typ, "_marshmallow_args", {}), **metadata, } field = getattr(typ, "_marshmallow_field", None) if field: return field(**metadata) else: return field_for_schema( newtype_supertype, metadata=metadata, default=default, base_schema=base_schema, ) # enumerations if isinstance(typ, EnumMeta): import marshmallow_enum return marshmallow_enum.EnumField(typ, **metadata) # Nested marshmallow dataclass nested_schema = getattr(typ, "Schema", None) # Nested dataclasses forward_reference = getattr(typ, "__forward_arg__", None) nested = (nested_schema or forward_reference or class_schema(typ, base_schema=base_schema)) return marshmallow.fields.Nested(nested, **metadata)
class IntStrSchema(marshmallow.Schema): """Schema with int and str candidates.""" x = marshmallow_union.Union( [marshmallow.fields.Int(), marshmallow.fields.String()])
def field_for_schema( typ: type, default=marshmallow.missing, metadata: t.Mapping[str, t.Any] = None) -> marshmallow.fields.Field: """ Get a marshmallow Field corresponding to the given python type. The metadata of the dataclass field is used as arguments to the marshmallow Field. >>> int_field = field_for_schema(int, default=9, metadata=dict(required=True)) >>> int_field.__class__ <class 'marshmallow.fields.Integer'> >>> int_field.default 9 >>> field_for_schema(t.Dict[str,str]).__class__ <class 'marshmallow.fields.Dict'> >>> field_for_schema(t.Optional[str]).__class__ <class 'marshmallow.fields.String'> >>> import marshmallow_enum >>> field_for_schema(enum.Enum("X", "a b c")).__class__ <class 'marshmallow_enum.EnumField'> >>> import typing >>> field_for_schema(t.Union[int,str]).__class__ <class 'marshmallow_union.Union'> >>> field_for_schema(t.NewType('UserId', int)).__class__ <class 'marshmallow.fields.Integer'> >>> field_for_schema(t.NewType('UserId', int), default=0).default 0 >>> class Color(enum.Enum): ... red = 1 >>> field_for_schema(Color).__class__ <class 'marshmallow_enum.EnumField'> >>> field_for_schema(t.Any).__class__ <class 'marshmallow.fields.Raw'> """ if metadata is None: metadata = {} else: metadata = dict(metadata) desert_metadata = dict(metadata).get(_DESERT_SENTINEL, {}) metadata[_DESERT_SENTINEL] = desert_metadata if default is not marshmallow.missing: desert_metadata.setdefault("default", default) desert_metadata.setdefault("allow_none", True) desert_metadata.setdefault("missing", default) field = None # If the field was already defined by the user predefined_field = desert_metadata.get("marshmallow_field") if predefined_field: field = predefined_field field.metadata.update(metadata) return field # Base types if not field and typ in _native_to_marshmallow: field = _native_to_marshmallow[typ](default=default) # Generic types origin = typing_inspect.get_origin(typ) if origin: arguments = typing_inspect.get_args(typ, True) if origin in (list, t.List): field = marshmallow.fields.List(field_for_schema(arguments[0])) if origin in (tuple, t.Tuple) and Ellipsis not in arguments: field = marshmallow.fields.Tuple( tuple(field_for_schema(arg) for arg in arguments)) elif origin in (tuple, t.Tuple) and Ellipsis in arguments: field = VariadicTuple( field_for_schema( only(arg for arg in arguments if arg != Ellipsis))) elif origin in (dict, t.Dict): field = marshmallow.fields.Dict( keys=field_for_schema(arguments[0]), values=field_for_schema(arguments[1]), ) elif typing_inspect.is_optional_type(typ): [subtyp] = (t for t in arguments if t is not NoneType) # Treat optional types as types with a None default metadata[_DESERT_SENTINEL]["default"] = metadata.get( "default", None) metadata[_DESERT_SENTINEL]["missing"] = metadata.get( "missing", None) metadata[_DESERT_SENTINEL]["required"] = False field = field_for_schema(subtyp, metadata=metadata, default=None) field.default = None field.missing = None field.allow_none = True elif typing_inspect.is_union_type(typ): subfields = [field_for_schema(subtyp) for subtyp in arguments] import marshmallow_union field = marshmallow_union.Union(subfields) # t.NewType returns a function with a __supertype__ attribute newtype_supertype = getattr(typ, "__supertype__", None) if newtype_supertype and inspect.isfunction(typ): metadata.setdefault("description", typ.__name__) field = field_for_schema(newtype_supertype, default=default) # enumerations if type(typ) is enum.EnumMeta: import marshmallow_enum field = marshmallow_enum.EnumField(typ, metadata=metadata) # Nested dataclasses forward_reference = getattr(typ, "__forward_arg__", None) if field is None: nested = forward_reference or class_schema(typ) field = marshmallow.fields.Nested(nested) field.metadata.update(metadata) for key in ["default", "missing", "required", "marshmallow_field"]: if key in metadata.keys(): metadata[_DESERT_SENTINEL][key] = metadata.pop(key) if field.default == field.missing == default == marshmallow.missing: field.required = True return field
def field_for_schema( typ: type, default=marshmallow.missing, metadata: Mapping[str, Any] = None, base_schema: Optional[Type[marshmallow.Schema]] = None, ) -> marshmallow.fields.Field: """ Get a marshmallow Field corresponding to the given python type. The metadata of the dataclass field is used as arguments to the marshmallow Field. :param base_schema: marshmallow schema used as a base class when deriving dataclass schema >>> int_field = field_for_schema(int, default=9, metadata=dict(required=True)) >>> int_field.__class__ <class 'marshmallow.fields.Integer'> >>> int_field.default 9 >>> int_field.required True >>> field_for_schema(Dict[str,str]).__class__ <class 'marshmallow.fields.Dict'> >>> field_for_schema(str, metadata={"marshmallow_field": marshmallow.fields.Url()}).__class__ <class 'marshmallow.fields.Url'> >>> field_for_schema(Optional[str]).__class__ <class 'marshmallow.fields.String'> >>> from enum import Enum >>> import marshmallow_enum >>> field_for_schema(Enum("X", "a b c")).__class__ <class 'marshmallow_enum.EnumField'> >>> import typing >>> field_for_schema(typing.Union[int,str]).__class__ <class 'marshmallow_union.Union'> >>> field_for_schema(typing.NewType('UserId', int)).__class__ <class 'marshmallow.fields.Integer'> >>> field_for_schema(typing.NewType('UserId', int), default=0).default 0 >>> class Color(Enum): ... red = 1 >>> field_for_schema(Color).__class__ <class 'marshmallow_enum.EnumField'> >>> field_for_schema(Any).__class__ <class 'marshmallow.fields.Raw'> """ metadata = {} if metadata is None else dict(metadata) if default is not marshmallow.missing: metadata.setdefault("default", default) if not metadata.get( "required"): # 'missing' must not be set for required fields. metadata.setdefault("missing", default) else: metadata.setdefault("required", True) # If the field was already defined by the user predefined_field = metadata.get("marshmallow_field") if predefined_field: return predefined_field # Base types if typ in _native_to_marshmallow: return _native_to_marshmallow[typ](**metadata) # Generic types origin = typing_inspect.get_origin(typ) if origin: arguments = typing_inspect.get_args(typ, True) if origin in (list, List): return marshmallow.fields.List( field_for_schema(arguments[0], base_schema=base_schema), **metadata) if origin in (tuple, Tuple): return marshmallow.fields.Tuple( tuple( field_for_schema(arg, base_schema=base_schema) for arg in arguments), **metadata, ) elif origin in (dict, Dict): return marshmallow.fields.Dict( keys=field_for_schema(arguments[0], base_schema=base_schema), values=field_for_schema(arguments[1], base_schema=base_schema), **metadata, ) elif typing_inspect.is_optional_type(typ): subtyp = next(t for t in arguments if t is not NoneType) # type: ignore # Treat optional types as types with a None default metadata["default"] = metadata.get("default", None) metadata["missing"] = metadata.get("missing", None) metadata["required"] = False return field_for_schema(subtyp, metadata=metadata, base_schema=base_schema) elif typing_inspect.is_union_type(typ): subfields = [ field_for_schema(subtyp, metadata=metadata, base_schema=base_schema) for subtyp in arguments ] import marshmallow_union return marshmallow_union.Union(subfields, **metadata) # typing.NewType returns a function with a __supertype__ attribute newtype_supertype = getattr(typ, "__supertype__", None) if newtype_supertype and inspect.isfunction(typ): # Add the information coming our custom NewType implementation metadata = { "description": typ.__name__, **getattr(typ, "_marshmallow_args", {}), **metadata, } field = getattr(typ, "_marshmallow_field", None) if field: return field(**metadata) else: return field_for_schema( newtype_supertype, metadata=metadata, default=default, base_schema=base_schema, ) # enumerations if type(typ) is EnumMeta: import marshmallow_enum return marshmallow_enum.EnumField(typ, **metadata) # Nested dataclasses forward_reference = getattr(typ, "__forward_arg__", None) nested = forward_reference or class_schema(typ, base_schema=base_schema) return marshmallow.fields.Nested(nested, **metadata)
def field_for_schema( typ: type, default=marshmallow.missing, metadata: Mapping[str, Any] = None) -> marshmallow.fields.Field: """ Get a marshmallow Field corresponding to the given python type. The metadata of the dataclass field is used as arguments to the marshmallow Field. >>> int_field = field_for_schema(int, default=9, metadata=dict(required=True)) >>> int_field.__class__ <class 'marshmallow.fields.Integer'> >>> int_field.default 9 >>> field_for_schema(Dict[str,str]).__class__ <class 'marshmallow.fields.Dict'> >>> field_for_schema(Optional[str]).__class__ <class 'marshmallow.fields.String'> >>> import marshmallow_enum >>> field_for_schema(Enum("X", "a b c")).__class__ <class 'marshmallow_enum.EnumField'> >>> import typing >>> field_for_schema(typing.Union[int,str]).__class__ <class 'marshmallow_union.Union'> >>> field_for_schema(NewType('UserId', int)).__class__ <class 'marshmallow.fields.Integer'> >>> field_for_schema(NewType('UserId', int), default=0).default 0 >>> class Color(Enum): ... red = 1 >>> field_for_schema(Color).__class__ <class 'marshmallow_enum.EnumField'> >>> field_for_schema(Any).__class__ <class 'marshmallow.fields.Raw'> """ metadata = {} if metadata is None else dict(metadata) if default is not marshmallow.missing: metadata.setdefault("default", default) if not metadata.get( "required"): # 'missing' must not be set for required fields. metadata.setdefault("missing", default) else: metadata.setdefault("required", True) # If the field was already defined by the user predefined_field = metadata.get("marshmallow_field") if predefined_field: return predefined_field # Base types if typ in _native_to_marshmallow: return _native_to_marshmallow[typ](**metadata) # Generic types origin = typing_inspect.get_origin(typ) if origin: arguments = typing_inspect.get_args(typ, True) if origin in (list, List): return marshmallow.fields.List(field_for_schema(arguments[0]), **metadata) if origin in (tuple, Tuple): return marshmallow.fields.Tuple( tuple(field_for_schema(arg) for arg in arguments), **metadata) elif origin in (dict, Dict): return marshmallow.fields.Dict( keys=field_for_schema(arguments[0]), values=field_for_schema(arguments[1]), **metadata, ) elif typing_inspect.is_optional_type(typ): subtyp = next(t for t in arguments if t is not NoneType) # Treat optional types as types with a None default metadata["default"] = metadata.get("default", None) metadata["missing"] = metadata.get("missing", None) metadata["required"] = False return field_for_schema(subtyp, metadata=metadata) elif typing_inspect.is_union_type(typ): subfields = [ field_for_schema(subtyp, metadata=metadata) for subtyp in arguments ] import marshmallow_union return marshmallow_union.Union(subfields, **metadata) # typing.NewType returns a function with a __supertype__ attribute newtype_supertype = getattr(typ, "__supertype__", None) if newtype_supertype and inspect.isfunction(typ): metadata.setdefault("description", typ.__name__) return field_for_schema(newtype_supertype, metadata=metadata, default=default) # enumerations if type(typ) is EnumMeta: import marshmallow_enum return marshmallow_enum.EnumField(typ, **metadata) # Nested dataclasses forward_reference = getattr(typ, "__forward_arg__", None) nested = forward_reference or class_schema(typ) try: nested.help = typ.__doc__ except AttributeError: # TODO need to handle the case where nested is a string forward reference. pass return marshmallow.fields.Nested(nested, **metadata)