Repository: marshmallow-code/apispec
Pull request #478 (2019-07-22 21:40:12)

Make field2property extensible
I took a shot at making `field2property` extensible for users to add custom attribute functions that can add attributes to the property dictionary.  

- This pull request extracts the logic for converting fields to properties into a separate class: `FieldConverter`.
- Extracts the functionality for handling Nested, List, and Dict fields into separate methods
- Adds a mechanism for users to add custom functions for handling specific fields or really anything.  [This test](https://github.com/marshmallow-code/apispec/compare/rework-name-resolver...extensible-field2property?expand=1#diff-aa718ad3528adfb73e9ad7101494e2c8R277) illustrates how a user would be able to extend `field2property` for a specific field.

I think that this is probably fine for most applications that I have seen in the issues.  There are a couple of things I'm not super happy with: 
- ~~Many of the attribute functions we have today depend on being a method so that they can have access to `self`.  For example `field2write_only` relies on the OpenAPI version number.  The logic for handling Nested, List, and Dictionary fields rely on calling other methods `resolve_nested_field` and `field2property`. The current mechansim wouldn't allow for that flexibility.~~
- ~~Right now I have `MarshmallowPlugin` initialized with parameters that only needs to pass on to other classes.  Might make more sense to allow `MarshmallowPlugin` to be initialized with an instance of `OpenAPIConverter` and then update it when `init_spec` is called.  Or maybe there is something else we can do.~~

Note: I have this pull request targeted to my latest branch to make the changes easier to understand.

lafrech: Sounds good.

Currently away. I'll check this when back home.

-- 
Jérôme


Bangertm: Updated the method for adding resolver functions and am now providing a way to allow those functions to have access to the FieldConverter instance so that they call other methods like `resolve_nested_schema` or `field2property`.
lafrech: Although #476 was merged, GH still presents the whole diff. Do you think you could ensure it only shows the diff from this PR only? You could close and recreate a new PR, but maybe it works by just push-forcing, I don't know.
Bangertm: Just rebased and forced pushed.  I would want to squash the last two commits before merging.  Leaving here for now to show multiple ways this could work. 
lafrech: I just pushed an additional commit in which I change `FieldConverter` into `FieldConverterMixin`.

This makes it closes to former design while still separating responsibilities.

Just a suggestion, we can revert it. Feedback welcome.

@Bangertm if that suits you, I think we can merge.

(There's a typo to fix in field_converter.py `s/propetry/property`. Can be done in another commit or in a rebase.)

Patch:
diff --git a/CHANGELOG.rst b/CHANGELOG.rst
index 9199f02..cd88d68 100644
--- a/CHANGELOG.rst
+++ b/CHANGELOG.rst
@@ -6,6 +6,8 @@ Changelog
 
 Features:
 
+- Add support for generating user defined OpenAPI properties for custom field
+  classes via an ``add_attribute_function`` method. (:pr:`478`)
 - *Backwards-incompatible*: The ``schema_name_resolver`` function now receives
   a ``Schema`` class, a ``Schema`` instance or a string that resolves to a
   ``Schema`` class. This allows a custom resolver to generate different names
diff --git a/docs/api_ext.rst b/docs/api_ext.rst
index 90a9f98..58ed806 100644
--- a/docs/api_ext.rst
+++ b/docs/api_ext.rst
@@ -13,6 +13,12 @@ apispec.ext.marshmallow.openapi
 .. automodule:: apispec.ext.marshmallow.openapi
     :members:
 
+apispec.ext.marshmallow.field_converter
++++++++++++++++++++++++++++++++++++++++
+
+.. automodule:: apispec.ext.marshmallow.field_converter
+    :members:
+
 apispec.ext.marshmallow.common
 ++++++++++++++++++++++++++++++
 .. automodule:: apispec.ext.marshmallow.common
diff --git a/docs/using_plugins.rst b/docs/using_plugins.rst
index 7f6d500..da3f831 100644
--- a/docs/using_plugins.rst
+++ b/docs/using_plugins.rst
@@ -252,6 +252,9 @@ OpenAPI type and format, or a marshmallow `Field` that has the desired target ma
 
     ma_plugin = MarshmallowPlugin()
 
+    spec = APISpec(
+        title="Demo", version="0.1", openapi_version="3.0.0", plugins=(ma_plugin,)
+    )
 
     # Inherits Integer mapping of ('integer', 'int32')
     class MyCustomInteger(Integer):
@@ -268,7 +271,24 @@ OpenAPI type and format, or a marshmallow `Field` that has the desired target ma
     class MyCustomFieldThatsKindaLikeAnInteger(Field):
         pass
 
+In situations where greater control of the properties generated for a custom field
+is desired, users may add custom logic to the conversion of fields to OpenAPI properties
+through the use of the `add_attribute_function
+<apispec.ext.marshmallow.field_converter.FieldConverterMixin.add_attribute_function>`
+method. Continuing from the example above:
+
+.. code-block:: python
+
+    def my_custom_field2properties(field, **kwargs):
+        """Add an OpenAPI extension flag to MyCustomField instances
+        """
+        ret = {}
+        if isinstance(field, MyCustomField):
+            ret["x-customField"] = True
+        return ret
+
 
+    ma_plugin.openapi.add_attribute_function(my_custom_field2properties)
 
 Next Steps
 ----------
diff --git a/src/apispec/ext/marshmallow/field_converter.py b/src/apispec/ext/marshmallow/field_converter.py
new file mode 100644
index 0000000..68c4fea
--- /dev/null
+++ b/src/apispec/ext/marshmallow/field_converter.py
@@ -0,0 +1,461 @@
+"""Utilities for generating OpenAPI Specification (fka Swagger) entities from
+:class:`Fields <marshmallow.fields.Field>`.
+
+.. warning::
+
+    This module is treated as private API.
+    Users should not need to use this module directly.
+"""
+import functools
+import operator
+import warnings
+
+import marshmallow
+from marshmallow.orderedset import OrderedSet
+
+from apispec.compat import RegexType, iteritems
+
+
+MARSHMALLOW_VERSION_INFO = tuple(
+    [int(part) for part in marshmallow.__version__.split(".") if part.isdigit()]
+)
+
+
+# marshmallow field => (JSON Schema type, format)
+DEFAULT_FIELD_MAPPING = {
+    marshmallow.fields.Integer: ("integer", "int32"),
+    marshmallow.fields.Number: ("number", None),
+    marshmallow.fields.Float: ("number", "float"),
+    marshmallow.fields.Decimal: ("number", None),
+    marshmallow.fields.String: ("string", None),
+    marshmallow.fields.Boolean: ("boolean", None),
+    marshmallow.fields.UUID: ("string", "uuid"),
+    marshmallow.fields.DateTime: ("string", "date-time"),
+    marshmallow.fields.Date: ("string", "date"),
+    marshmallow.fields.Time: ("string", None),
+    marshmallow.fields.Email: ("string", "email"),
+    marshmallow.fields.URL: ("string", "url"),
+    marshmallow.fields.Dict: ("object", None),
+    # Assume base Field and Raw are strings
+    marshmallow.fields.Field: ("string", None),
+    marshmallow.fields.Raw: ("string", None),
+    marshmallow.fields.List: ("array", None),
+}
+
+
+# Properties that may be defined in a field's metadata that will be added to the output
+# of field2property
+# https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#schemaObject
+_VALID_PROPERTIES = {
+    "format",
+    "title",
+    "description",
+    "default",
+    "multipleOf",
+    "maximum",
+    "exclusiveMaximum",
+    "minimum",
+    "exclusiveMinimum",
+    "maxLength",
+    "minLength",
+    "pattern",
+    "maxItems",
+    "minItems",
+    "uniqueItems",
+    "maxProperties",
+    "minProperties",
+    "required",
+    "enum",
+    "type",
+    "items",
+    "allOf",
+    "properties",
+    "additionalProperties",
+    "readOnly",
+    "xml",
+    "externalDocs",
+    "example",
+}
+
+
+_VALID_PREFIX = "x-"
+
+
+class FieldConverterMixin(object):
+    """Mixin class to convert fields to an OpenAPI property"""
+
+    field_mapping = DEFAULT_FIELD_MAPPING
+
+    def init_attribute_functions(self):
+        self.attribute_functions = [
+            self.field2type_and_format,
+            self.field2default,
+            self.field2choices,
+            self.field2read_only,
+            self.field2write_only,
+            self.field2nullable,
+            self.field2range,
+            self.field2length,
+            self.field2pattern,
+            self.metadata2properties,
+            self.nested2properties,
+            self.list2properties,
+            self.dict2properties,
+        ]
+
+    def map_to_openapi_type(self, *args):
+        """Decorator to set mapping for custom fields.
+
+        ``*args`` can be:
+
+        - a pair of the form ``(type, format)``
+        - a core marshmallow field type (in which case we reuse that type's mapping)
+        """
+        if len(args) == 1 and args[0] in self.field_mapping:
+            openapi_type_field = self.field_mapping[args[0]]
+        elif len(args) == 2:
+            openapi_type_field = args
+        else:
+            raise TypeError("Pass core marshmallow field type or (type, fmt) pair.")
+
+        def inner(field_type):
+            self.field_mapping[field_type] = openapi_type_field
+            return field_type
+
+        return inner
+
+    def add_attribute_function(self, func):
+        """Method to add an attribute function to the list of attribute functions
+        that will be called on a field to convert it from a field to an OpenAPI
+        property
+
+        :param func func: the attribute function to add - will be called for each
+            field in a schema with a `field <marshmallow.fields.Field>` instance
+            positional argument and `self <apispec.ext.marshmallow.openapi.OpenAPIConverter>`
+            and `ret <dict>` keyword arguments.
+            Must return a dictionary of OpenAPI properties that will be shallow
+            merged with the return values of all other attribute functions called on the field.
+            User added attribute functions will be called after all built-in attribute
+            functions in the order they were added. The merged results of all
+            previously called attribute functions are accessable via the `ret`
+            argument.
+        """
+        self.attribute_functions.append(functools.partial(func, self=self))
+
+    def field2property(self, field):
+        """Return the JSON Schema property definition given a marshmallow
+        :class:`Field <marshmallow.fields.Field>`.
+
+        Will include field metadata that are valid properties of OpenAPI schema objects
+        (e.g. "description", "enum", "example").
+
+        https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#schemaObject
+
+        :param Field field: A marshmallow field.
+        :rtype: dict, a Property Object
+        """
+        ret = {}
+
+        for attr_func in self.attribute_functions:
+            ret.update(attr_func(field, ret=ret))
+
+        return ret
+
+    def field2type_and_format(self, field, **kwargs):
+        """Return the dictionary of OpenAPI type and format based on the field
+        type
+
+        :param Field field: A marshmallow field.
+        :rtype: dict
+        """
+        # If this type isn't directly in the field mapping then check the
+        # hierarchy until we find something that does.
+        for field_class in type(field).__mro__:
+            if field_class in self.field_mapping:
+                type_, fmt = self.field_mapping[field_class]
+                break
+        else:
+            warnings.warn(
+                "Field of type {} does not inherit from marshmallow.Field.".format(
+                    type(field)
+                ),
+                UserWarning,
+            )
+            type_, fmt = "string", None
+
+        ret = {"type": type_}
+
+        if fmt:
+            ret["format"] = fmt
+
+        return ret
+
+    def field2default(self, field, **kwargs):
+        """Return the dictionary containing the field's default value
+
+        Will first look for a `doc_default` key in the field's metadata and then
+        fall back on the field's `missing` parameter. A callable passed to the
+        field's missing parameter will be ignored.
+
+        :param Field field: A marshmallow field.
+        :rtype: dict
+        """
+        ret = {}
+        if "doc_default" in field.metadata:
+            ret["default"] = field.metadata["doc_default"]
+        else:
+            default = field.missing
+            if default is not marshmallow.missing and not callable(default):
+                ret["default"] = default
+
+        return ret
+
+    def field2choices(self, field, **kwargs):
+        """Return the dictionary of OpenAPI field attributes for valid choices definition
+
+        :param Field field: A marshmallow field.
+        :rtype: dict
+        """
+        attributes = {}
+
+        comparable = [
+            validator.comparable
+            for validator in field.validators
+            if hasattr(validator, "comparable")
+        ]
+        if comparable:
+            attributes["enum"] = comparable
+        else:
+            choices = [
+                OrderedSet(validator.choices)
+                for validator in field.validators
+                if hasattr(validator, "choices")
+            ]
+            if choices:
+                attributes["enum"] = list(functools.reduce(operator.and_, choices))
+
+        return attributes
+
+    def field2read_only(self, field, **kwargs):
+        """Return the dictionary of OpenAPI field attributes for a dump_only field.
+
+        :param Field field: A marshmallow field.
+        :rtype: dict
+        """
+        attributes = {}
+        if field.dump_only:
+            attributes["readOnly"] = True
+        return attributes
+
+    def field2write_only(self, field, **kwargs):
+        """Return the dictionary of OpenAPI field attributes for a load_only field.
+
+        :param Field field: A marshmallow field.
+        :rtype: dict
+        """
+        attributes = {}
+        if field.load_only and self.openapi_version.major >= 3:
+            attributes["writeOnly"] = True
+        return attributes
+
+    def field2nullable(self, field, **kwargs):
+        """Return the dictionary of OpenAPI field attributes for a nullable field.
+
+        :param Field field: A marshmallow field.
+        :rtype: dict
+        """
+        attributes = {}
+        if field.allow_none:
+            attributes[
+                "x-nullable" if self.openapi_version.major < 3 else "nullable"
+            ] = True
+        return attributes
+
+    def field2range(self, field, **kwargs):
+        """Return the dictionary of OpenAPI field attributes for a set of
+        :class:`Range <marshmallow.validators.Range>` validators.
+
+        :param Field field: A marshmallow field.
+        :rtype: dict
+        """
+        validators = [
+            validator
+            for validator in field.validators
+            if (
+                hasattr(validator, "min")
+                and hasattr(validator, "max")
+                and not hasattr(validator, "equal")
+            )
+        ]
+
+        attributes = {}
+        for validator in validators:
+            if validator.min is not None:
+                if hasattr(attributes, "minimum"):
+                    attributes["minimum"] = max(attributes["minimum"], validator.min)
+                else:
+                    attributes["minimum"] = validator.min
+            if validator.max is not None:
+                if hasattr(attributes, "maximum"):
+                    attributes["maximum"] = min(attributes["maximum"], validator.max)
+                else:
+                    attributes["maximum"] = validator.max
+        return attributes
+
+    def field2length(self, field, **kwargs):
+        """Return the dictionary of OpenAPI field attributes for a set of
+        :class:`Length <marshmallow.validators.Length>` validators.
+
+        :param Field field: A marshmallow field.
+        :rtype: dict
+        """
+        attributes = {}
+
+        validators = [
+            validator
+            for validator in field.validators
+            if (
+                hasattr(validator, "min")
+                and hasattr(validator, "max")
+                and hasattr(validator, "equal")
+            )
+        ]
+
+        is_array = isinstance(
+            field, (marshmallow.fields.Nested, marshmallow.fields.List)
+        )
+        min_attr = "minItems" if is_array else "minLength"
+        max_attr = "maxItems" if is_array else "maxLength"
+
+        for validator in validators:
+            if validator.min is not None:
+                if hasattr(attributes, min_attr):
+                    attributes[min_attr] = max(attributes[min_attr], validator.min)
+                else:
+                    attributes[min_attr] = validator.min
+            if validator.max is not None:
+                if hasattr(attributes, max_attr):
+                    attributes[max_attr] = min(attributes[max_attr], validator.max)
+                else:
+                    attributes[max_attr] = validator.max
+
+        for validator in validators:
+            if validator.equal is not None:
+                attributes[min_attr] = validator.equal
+                attributes[max_attr] = validator.equal
+        return attributes
+
+    def field2pattern(self, field, **kwargs):
+        """Return the dictionary of OpenAPI field attributes for a set of
+        :class:`Range <marshmallow.validators.Regexp>` validators.
+
+        :param Field field: A marshmallow field.
+        :rtype: dict
+        """
+        regex_validators = (
+            v
+            for v in field.validators
+            if isinstance(getattr(v, "regex", None), RegexType)
+        )
+        v = next(regex_validators, None)
+        attributes = {} if v is None else {"pattern": v.regex.pattern}
+
+        if next(regex_validators, None) is not None:
+            warnings.warn(
+                "More than one regex validator defined on {} field. Only the "
+                "first one will be used in the output spec.".format(type(field)),
+                UserWarning,
+            )
+
+        return attributes
+
+    def metadata2properties(self, field, **kwargs):
+        """Return a dictionary of properties extracted from field Metadata
+
+        Will include field metadata that are valid properties of `OpenAPI schema
+        objects
+        <https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#schemaObject>`_
+        (e.g. "description", "enum", "example").
+
+        In addition, `specification extensions
+        <https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#specification-extensions>`_
+        are supported.  Prefix `x_` to the desired extension when passing the
+        keyword argument to the field constructor. apispec will convert `x_` to
+        `x-` to comply with OpenAPI.
+
+        :param Field field: A marshmallow field.
+        :rtype: dict
+        """
+        # Dasherize metadata that starts with x_
+        metadata = {
+            key.replace("_", "-") if key.startswith("x_") else key: value
+            for key, value in iteritems(field.metadata)
+        }
+
+        # Avoid validation error with "Additional properties not allowed"
+        ret = {
+            key: value
+            for key, value in metadata.items()
+            if key in _VALID_PROPERTIES or key.startswith(_VALID_PREFIX)
+        }
+        return ret
+
+    def nested2properties(self, field, ret):
+        """Return a dictionary of properties from :class:`Nested <marshmallow.fields.Nested`
+        fields
+
+        Typically provides a reference object and will add the schema to the spec
+        if it is not already present
+        If a custom `schema_name_resolver` function returns `None` for the nested
+        schema a JSON schema object will be returned
+
+        :param Field field: A marshmallow field.
+        :rtype: dict
+        """
+        if isinstance(field, marshmallow.fields.Nested):
+            del ret["type"]
+            schema_dict = self.resolve_nested_schema(field.schema)
+            if ret and "$ref" in schema_dict:
+                ret.update({"allOf": [schema_dict]})
+            else:
+                ret.update(schema_dict)
+        return ret
+
+    def list2properties(self, field, **kwargs):
+        """Return a dictionary of properties from :class:`List <marshmallow.fields.List`
+        fields
+
+        Will provide an `items` property based on the field's `inner` attribute
+
+        :param Field field: A marshmallow field.
+        :rtype: dict
+        """
+        ret = {}
+        if isinstance(field, marshmallow.fields.List):
+            # field.container was renamed to field.inner in marshmallow 3.0.0rc8
+            inner_field = field.inner if hasattr(field, "inner") else field.container
+            ret["items"] = self.field2property(inner_field)
+        return ret
+
+    def dict2properties(self, field, **kwargs):
+        """Return a dictionary of properties from :class:`Dict <marshmallow.fields.Dict`
+        fields
+
+        Only applicable for Marshmallow versions greater than 3. Will provide an
+        `additionalProperties` property based on the field's `value_field` attribute
+
+        :param Field field: A marshmallow field.
+        :rtype: dict
+        """
+        ret = {}
+        if isinstance(field, marshmallow.fields.Dict):
+            if MARSHMALLOW_VERSION_INFO[0] >= 3:
+                # field.value_container was renamed to field.value_field in marshmallow 3.0.0rc8
+                value_field = (
+                    field.value_field
+                    if hasattr(field, "value_field")
+                    else field.value_container
+                )
+                if value_field:
+                    ret["additionalProperties"] = self.field2property(value_field)
+        return ret
diff --git a/src/apispec/ext/marshmallow/openapi.py b/src/apispec/ext/marshmallow/openapi.py
index a79093c..4220032 100644
--- a/src/apispec/ext/marshmallow/openapi.py
+++ b/src/apispec/ext/marshmallow/openapi.py
@@ -8,51 +8,27 @@ marshmallow :class:`Schemas <marshmallow.Schema>` and :class:`Fields <marshmallo
     Users should not need to use this module directly.
 """
 from __future__ import absolute_import, unicode_literals
-import operator
-import functools
-import warnings
 from collections import OrderedDict
 
 import marshmallow
 from marshmallow.utils import is_collection
-from marshmallow.orderedset import OrderedSet
 
-from apispec.compat import RegexType, iteritems
+from apispec.compat import iteritems
 from apispec.utils import OpenAPIVersion, build_reference
+from apispec.exceptions import APISpecError
+from .field_converter import FieldConverterMixin
 from .common import (
     get_fields,
     make_schema_key,
     resolve_schema_instance,
     get_unique_schema_name,
 )
-from apispec.exceptions import APISpecError
 
 
 MARSHMALLOW_VERSION_INFO = tuple(
     [int(part) for part in marshmallow.__version__.split(".") if part.isdigit()]
 )
 
-# marshmallow field => (JSON Schema type, format)
-DEFAULT_FIELD_MAPPING = {
-    marshmallow.fields.Integer: ("integer", "int32"),
-    marshmallow.fields.Number: ("number", None),
-    marshmallow.fields.Float: ("number", "float"),
-    marshmallow.fields.Decimal: ("number", None),
-    marshmallow.fields.String: ("string", None),
-    marshmallow.fields.Boolean: ("boolean", None),
-    marshmallow.fields.UUID: ("string", "uuid"),
-    marshmallow.fields.DateTime: ("string", "date-time"),
-    marshmallow.fields.Date: ("string", "date"),
-    marshmallow.fields.Time: ("string", None),
-    marshmallow.fields.Email: ("string", "email"),
-    marshmallow.fields.URL: ("string", "url"),
-    marshmallow.fields.Dict: ("object", None),
-    # Assume base Field and Raw are strings
-    marshmallow.fields.Field: ("string", None),
-    marshmallow.fields.Raw: ("string", None),
-    marshmallow.fields.List: ("array", None),
-}
-
 
 __location_map__ = {
     "query": "query",
@@ -65,58 +41,26 @@ __location_map__ = {
 }
 
 
-# Properties that may be defined in a field's metadata that will be added to the output
-# of field2property
-# https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#schemaObject
-_VALID_PROPERTIES = {
-    "format",
-    "title",
-    "description",
-    "default",
-    "multipleOf",
-    "maximum",
-    "exclusiveMaximum",
-    "minimum",
-    "exclusiveMinimum",
-    "maxLength",
-    "minLength",
-    "pattern",
-    "maxItems",
-    "minItems",
-    "uniqueItems",
-    "maxProperties",
-    "minProperties",
-    "required",
-    "enum",
-    "type",
-    "items",
-    "allOf",
-    "properties",
-    "additionalProperties",
-    "readOnly",
-    "xml",
-    "externalDocs",
-    "example",
-}
-
-_VALID_PREFIX = "x-"
-
-
-class OpenAPIConverter(object):
+class OpenAPIConverter(FieldConverterMixin):
     """Converter generating OpenAPI specification from Marshmallow schemas and fields
 
     :param str|OpenAPIVersion openapi_version: The OpenAPI version to use.
         Should be in the form '2.x' or '3.x.x' to comply with the OpenAPI standard.
+    :param callable schema_name_resolver: Callable to generate the schema definition name.
+        Receives the `Schema` class and returns the name to be used in refs within
+        the generated spec. When working with circular referencing this function
+        must must not return `None` for schemas in a circular reference chain.
+    :param APISpec spec: An initalied spec. Nested schemas will be added to the
+        spec
     """
 
     def __init__(self, openapi_version, schema_name_resolver, spec):
         self.openapi_version = OpenAPIVersion(openapi_version)
         self.schema_name_resolver = schema_name_resolver
         self.spec = spec
+        self.init_attribute_functions()
         # Schema references
         self.refs = {}
-        #  Field mappings
-        self.field_mapping = DEFAULT_FIELD_MAPPING
 
     @staticmethod
     def _observed_name(field, name):
@@ -133,318 +77,6 @@ class OpenAPIConverter(object):
             return dump_to or load_from or name
         return field.data_key or name
 
-    def map_to_openapi_type(self, *args):
-        """Decorator to set mapping for custom fields.
-
-        ``*args`` can be:
-
-        - a pair of the form ``(type, format)``
-        - a core marshmallow field type (in which case we reuse that type's mapping)
-        """
-        if len(args) == 1 and args[0] in self.field_mapping:
-            openapi_type_field = self.field_mapping[args[0]]
-        elif len(args) == 2:
-            openapi_type_field = args
-        else:
-            raise TypeError("Pass core marshmallow field type or (type, fmt) pair.")
-
-        def inner(field_type):
-            self.field_mapping[field_type] = openapi_type_field
-            return field_type
-
-        return inner
-
-    def field2type_and_format(self, field):
-        """Return the dictionary of OpenAPI type and format based on the field
-        type
-
-        :param Field field: A marshmallow field.
-        :rtype: dict
-        """
-        # If this type isn't directly in the field mapping then check the
-        # hierarchy until we find something that does.
-        for field_class in type(field).__mro__:
-            if field_class in self.field_mapping:
-                type_, fmt = self.field_mapping[field_class]
-                break
-        else:
-            warnings.warn(
-                "Field of type {} does not inherit from marshmallow.Field.".format(
-                    type(field)
-                ),
-                UserWarning,
-            )
-            type_, fmt = "string", None
-
-        ret = {"type": type_}
-
-        if fmt:
-            ret["format"] = fmt
-
-        return ret
-
-    def field2default(self, field):
-        """Return the dictionary containing the field's default value
-
-        Will first look for a `doc_default` key in the field's metadata and then
-        fall back on the field's `missing` parameter. A callable passed to the
-        field's missing parameter will be ignored.
-
-        :param Field field: A marshmallow field.
-        :rtype: dict
-        """
-        ret = {}
-        if "doc_default" in field.metadata:
-            ret["default"] = field.metadata["doc_default"]
-        else:
-            default = field.missing
-            if default is not marshmallow.missing and not callable(default):
-                ret["default"] = default
-
-        return ret
-
-    def field2choices(self, field, **kwargs):
-        """Return the dictionary of OpenAPI field attributes for valid choices definition
-
-        :param Field field: A marshmallow field.
-        :rtype: dict
-        """
-        attributes = {}
-
-        comparable = [
-            validator.comparable
-            for validator in field.validators
-            if hasattr(validator, "comparable")
-        ]
-        if comparable:
-            attributes["enum"] = comparable
-        else:
-            choices = [
-                OrderedSet(validator.choices)
-                for validator in field.validators
-                if hasattr(validator, "choices")
-            ]
-            if choices:
-                attributes["enum"] = list(functools.reduce(operator.and_, choices))
-
-        return attributes
-
-    def field2read_only(self, field, **kwargs):
-        """Return the dictionary of OpenAPI field attributes for a dump_only field.
-
-        :param Field field: A marshmallow field.
-        :rtype: dict
-        """
-        attributes = {}
-        if field.dump_only:
-            attributes["readOnly"] = True
-        return attributes
-
-    def field2write_only(self, field, **kwargs):
-        """Return the dictionary of OpenAPI field attributes for a load_only field.
-
-        :param Field field: A marshmallow field.
-        :rtype: dict
-        """
-        attributes = {}
-        if field.load_only and self.openapi_version.major >= 3:
-            attributes["writeOnly"] = True
-        return attributes
-
-    def field2nullable(self, field, **kwargs):
-        """Return the dictionary of OpenAPI field attributes for a nullable field.
-
-        :param Field field: A marshmallow field.
-        :rtype: dict
-        """
-        attributes = {}
-        if field.allow_none:
-            attributes[
-                "x-nullable" if self.openapi_version.major < 3 else "nullable"
-            ] = True
-        return attributes
-
-    def field2range(self, field, **kwargs):
-        """Return the dictionary of OpenAPI field attributes for a set of
-        :class:`Range <marshmallow.validators.Range>` validators.
-
-        :param Field field: A marshmallow field.
-        :rtype: dict
-        """
-        validators = [
-            validator
-            for validator in field.validators
-            if (
-                hasattr(validator, "min")
-                and hasattr(validator, "max")
-                and not hasattr(validator, "equal")
-            )
-        ]
-
-        attributes = {}
-        for validator in validators:
-            if validator.min is not None:
-                if hasattr(attributes, "minimum"):
-                    attributes["minimum"] = max(attributes["minimum"], validator.min)
-                else:
-                    attributes["minimum"] = validator.min
-            if validator.max is not None:
-                if hasattr(attributes, "maximum"):
-                    attributes["maximum"] = min(attributes["maximum"], validator.max)
-                else:
-                    attributes["maximum"] = validator.max
-        return attributes
-
-    def field2length(self, field, **kwargs):
-        """Return the dictionary of OpenAPI field attributes for a set of
-        :class:`Length <marshmallow.validators.Length>` validators.
-
-        :param Field field: A marshmallow field.
-        :rtype: dict
-        """
-        attributes = {}
-
-        validators = [
-            validator
-            for validator in field.validators
-            if (
-                hasattr(validator, "min")
-                and hasattr(validator, "max")
-                and hasattr(validator, "equal")
-            )
-        ]
-
-        is_array = isinstance(
-            field, (marshmallow.fields.Nested, marshmallow.fields.List)
-        )
-        min_attr = "minItems" if is_array else "minLength"
-        max_attr = "maxItems" if is_array else "maxLength"
-
-        for validator in validators:
-            if validator.min is not None:
-                if hasattr(attributes, min_attr):
-                    attributes[min_attr] = max(attributes[min_attr], validator.min)
-                else:
-                    attributes[min_attr] = validator.min
-            if validator.max is not None:
-                if hasattr(attributes, max_attr):
-                    attributes[max_attr] = min(attributes[max_attr], validator.max)
-                else:
-                    attributes[max_attr] = validator.max
-
-        for validator in validators:
-            if validator.equal is not None:
-                attributes[min_attr] = validator.equal
-                attributes[max_attr] = validator.equal
-        return attributes
-
-    def field2pattern(self, field, **kwargs):
-        """Return the dictionary of OpenAPI field attributes for a set of
-        :class:`Range <marshmallow.validators.Regexp>` validators.
-
-        :param Field field: A marshmallow field.
-        :rtype: dict
-        """
-        regex_validators = (
-            v
-            for v in field.validators
-            if isinstance(getattr(v, "regex", None), RegexType)
-        )
-        v = next(regex_validators, None)
-        attributes = {} if v is None else {"pattern": v.regex.pattern}
-
-        if next(regex_validators, None) is not None:
-            warnings.warn(
-                "More than one regex validator defined on {} field. Only the "
-                "first one will be used in the output spec.".format(type(field)),
-                UserWarning,
-            )
-
-        return attributes
-
-    def metadata2properties(self, field):
-        """Return a dictionary of properties extracted from field Metadata
-
-        Will include field metadata that are valid properties of `OpenAPI schema
-        objects
-        <https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#schemaObject>`_
-        (e.g. “description”, “enum”, “example”).
-
-        In addition, `specification extensions
-        <https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#specification-extensions>`_
-        are supported.  Prefix `x_` to the desired extension when passing the
-        keyword argument to the field constructor. apispec will convert `x_` to
-        `x-` to comply with OpenAPI.
-
-        :param Field field: A marshmallow field.
-        :rtype: dict
-        """
-        # Dasherize metadata that starts with x_
-        metadata = {
-            key.replace("_", "-") if key.startswith("x_") else key: value
-            for key, value in iteritems(field.metadata)
-        }
-
-        # Avoid validation error with "Additional properties not allowed"
-        ret = {
-            key: value
-            for key, value in metadata.items()
-            if key in _VALID_PROPERTIES or key.startswith(_VALID_PREFIX)
-        }
-        return ret
-
-    def field2property(self, field):
-        """Return the JSON Schema property definition given a marshmallow
-        :class:`Field <marshmallow.fields.Field>`.
-
-        Will include field metadata that are valid properties of OpenAPI schema objects
-        (e.g. "description", "enum", "example").
-
-        https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#schemaObject
-
-        :param Field field: A marshmallow field.
-        :rtype: dict, a Property Object
-        """
-        ret = {}
-
-        for attr_func in (
-            self.field2type_and_format,
-            self.field2default,
-            self.field2choices,
-            self.field2read_only,
-            self.field2write_only,
-            self.field2nullable,
-            self.field2range,
-            self.field2length,
-            self.field2pattern,
-            self.metadata2properties,
-        ):
-            ret.update(attr_func(field))
-
-        if isinstance(field, marshmallow.fields.Nested):
-            del ret["type"]
-            schema_dict = self.resolve_nested_schema(field.schema)
-            if ret and "$ref" in schema_dict:
-                ret.update({"allOf": [schema_dict]})
-            else:
-                ret.update(schema_dict)
-        elif isinstance(field, marshmallow.fields.List):
-            # field.container was renamed to field.inner in marshmallow 3.0.0rc8
-            inner_field = field.inner if hasattr(field, "inner") else field.container
-            ret["items"] = self.field2property(inner_field)
-        elif isinstance(field, marshmallow.fields.Dict):
-            if MARSHMALLOW_VERSION_INFO[0] >= 3:
-                # field.value_container was renamed to field.value_field in marshmallow 3.0.0rc8
-                value_field = (
-                    field.value_field
-                    if hasattr(field, "value_field")
-                    else field.value_container
-                )
-                if value_field:
-                    ret["additionalProperties"] = self.field2property(value_field)
-
-        return ret
-
     def resolve_nested_schema(self, schema):
         """Return the Open API representation of a marshmallow Schema.
 


Test patch:
diff --git a/tests/schemas.py b/tests/schemas.py
index 1201438..ae03d99 100644
--- a/tests/schemas.py
+++ b/tests/schemas.py
@@ -59,3 +59,21 @@ class DefaultValuesSchema(Schema):
     string_callable_default = fields.Str(missing=lambda: "Callable")
     string_manual_default = fields.Str(missing=lambda: "Callable", doc_default="Manual")
     numbers = fields.List(fields.Int, missing=list)
+
+
+class CategorySchema(Schema):
+    id = fields.Int()
+    name = fields.Str(required=True)
+    breed = fields.Str(dump_only=True)
+
+
+class CustomList(fields.List):
+    pass
+
+
+class CustomStringField(fields.String):
+    pass
+
+
+class CustomIntegerField(fields.Integer):
+    pass
diff --git a/tests/test_ext_marshmallow_field.py b/tests/test_ext_marshmallow_field.py
new file mode 100644
index 0000000..0984ba2
--- /dev/null
+++ b/tests/test_ext_marshmallow_field.py
@@ -0,0 +1,290 @@
+# -*- coding: utf-8 -*-
+import re
+
+import pytest
+from marshmallow import fields, validate
+
+from apispec import APISpec
+from apispec.ext.marshmallow import MarshmallowPlugin
+
+from .schemas import CategorySchema, CustomList, CustomStringField, CustomIntegerField
+from .utils import build_ref
+
+
+def test_field2choices_preserving_order(openapi):
+    choices = ["a", "b", "c", "aa", "0", "cc"]
+    field = fields.String(validate=validate.OneOf(choices))
+    assert openapi.field2choices(field) == {"enum": choices}
+
+
+@pytest.mark.parametrize(
+    ("FieldClass", "jsontype"),
+    [
+        (fields.Integer, "integer"),
+        (fields.Number, "number"),
+        (fields.Float, "number"),
+        (fields.String, "string"),
+        (fields.Str, "string"),
+        (fields.Boolean, "boolean"),
+        (fields.Bool, "boolean"),
+        (fields.UUID, "string"),
+        (fields.DateTime, "string"),
+        (fields.Date, "string"),
+        (fields.Time, "string"),
+        (fields.Email, "string"),
+        (fields.URL, "string"),
+        # Assume base Field and Raw are strings
+        (fields.Field, "string"),
+        (fields.Raw, "string"),
+        # Custom fields inherit types from their parents
+        (CustomStringField, "string"),
+        (CustomIntegerField, "integer"),
+    ],
+)
+def test_field2property_type(FieldClass, jsontype, spec_fixture):
+    field = FieldClass()
+    res = spec_fixture.openapi.field2property(field)
+    assert res["type"] == jsontype
+
+
+@pytest.mark.parametrize("ListClass", [fields.List, CustomList])
+def test_formatted_field_translates_to_array(ListClass, spec_fixture):
+    field = ListClass(fields.String)
+    res = spec_fixture.openapi.field2property(field)
+    assert res["type"] == "array"
+    assert res["items"] == spec_fixture.openapi.field2property(fields.String())
+
+
+@pytest.mark.parametrize(
+    ("FieldClass", "expected_format"),
+    [
+        (fields.Integer, "int32"),
+        (fields.Float, "float"),
+        (fields.UUID, "uuid"),
+        (fields.DateTime, "date-time"),
+        (fields.Date, "date"),
+        (fields.Email, "email"),
+        (fields.URL, "url"),
+    ],
+)
+def test_field2property_formats(FieldClass, expected_format, spec_fixture):
+    field = FieldClass()
+    res = spec_fixture.openapi.field2property(field)
+    assert res["format"] == expected_format
+
+
+def test_field_with_description(spec_fixture):
+    field = fields.Str(description="a username")
+    res = spec_fixture.openapi.field2property(field)
+    assert res["description"] == "a username"
+
+
+def test_field_with_missing(spec_fixture):
+    field = fields.Str(default="foo", missing="bar")
+    res = spec_fixture.openapi.field2property(field)
+    assert res["default"] == "bar"
+
+
+def test_field_with_boolean_false_missing(spec_fixture):
+    field = fields.Boolean(default=None, missing=False)
+    res = spec_fixture.openapi.field2property(field)
+    assert res["default"] is False
+
+
+def test_field_with_missing_load(spec_fixture):
+    field = fields.Str(default="foo", missing="bar")
+    res = spec_fixture.openapi.field2property(field)
+    assert res["default"] == "bar"
+
+
+def test_field_with_boolean_false_missing_load(spec_fixture):
+    field = fields.Boolean(default=None, missing=False)
+    res = spec_fixture.openapi.field2property(field)
+    assert res["default"] is False
+
+
+def test_field_with_missing_callable(spec_fixture):
+    field = fields.Str(missing=lambda: "dummy")
+    res = spec_fixture.openapi.field2property(field)
+    assert "default" not in res
+
+
+def test_field_with_doc_default(spec_fixture):
+    field = fields.Str(doc_default="Manual default")
+    res = spec_fixture.openapi.field2property(field)
+    assert res["default"] == "Manual default"
+
+
+def test_field_with_doc_default_and_missing(spec_fixture):
+    field = fields.Int(doc_default=42, missing=12)
+    res = spec_fixture.openapi.field2property(field)
+    assert res["default"] == 42
+
+
+def test_field_with_choices(spec_fixture):
+    field = fields.Str(validate=validate.OneOf(["freddie", "brian", "john"]))
+    res = spec_fixture.openapi.field2property(field)
+    assert set(res["enum"]) == {"freddie", "brian", "john"}
+
+
+def test_field_with_equal(spec_fixture):
+    field = fields.Str(validate=validate.Equal("only choice"))
+    res = spec_fixture.openapi.field2property(field)
+    assert res["enum"] == ["only choice"]
+
+
+def test_only_allows_valid_properties_in_metadata(spec_fixture):
+    field = fields.Str(
+        missing="foo",
+        description="foo",
+        enum=["red", "blue"],
+        allOf=["bar"],
+        not_valid="lol",
+    )
+    res = spec_fixture.openapi.field2property(field)
+    assert res["default"] == field.missing
+    assert "description" in res
+    assert "enum" in res
+    assert "allOf" in res
+    assert "not_valid" not in res
+
+
+def test_field_with_choices_multiple(spec_fixture):
+    field = fields.Str(
+        validate=[
+            validate.OneOf(["freddie", "brian", "john"]),
+            validate.OneOf(["brian", "john", "roger"]),
+        ]
+    )
+    res = spec_fixture.openapi.field2property(field)
+    assert set(res["enum"]) == {"brian", "john"}
+
+
+def test_field_with_additional_metadata(spec_fixture):
+    field = fields.Str(minLength=6, maxLength=100)
+    res = spec_fixture.openapi.field2property(field)
+    assert res["maxLength"] == 100
+    assert res["minLength"] == 6
+
+
+def test_field_with_allow_none(spec_fixture):
+    field = fields.Str(allow_none=True)
+    res = spec_fixture.openapi.field2property(field)
+    if spec_fixture.openapi.openapi_version.major < 3:
+        assert res["x-nullable"] is True
+    else:
+        assert res["nullable"] is True
+
+
+def test_field_with_str_regex(spec_fixture):
+    regex_str = "^[a-zA-Z0-9]$"
+    field = fields.Str(validate=validate.Regexp(regex_str))
+    ret = spec_fixture.openapi.field2property(field)
+    assert ret["pattern"] == regex_str
+
+
+def test_field_with_pattern_obj_regex(spec_fixture):
+    regex_str = "^[a-zA-Z0-9]$"
+    field = fields.Str(validate=validate.Regexp(re.compile(regex_str)))
+    ret = spec_fixture.openapi.field2property(field)
+    assert ret["pattern"] == regex_str
+
+
+def test_field_with_no_pattern(spec_fixture):
+    field = fields.Str()
+    ret = spec_fixture.openapi.field2property(field)
+    assert "pattern" not in ret
+
+
+def test_field_with_multiple_patterns(recwarn, spec_fixture):
+    regex_validators = [validate.Regexp("winner"), validate.Regexp("loser")]
+    field = fields.Str(validate=regex_validators)
+    with pytest.warns(UserWarning, match="More than one regex validator"):
+        ret = spec_fixture.openapi.field2property(field)
+    assert ret["pattern"] == "winner"
+
+
+def test_field2property_nested_spec_metadatas(spec_fixture):
+    spec_fixture.spec.components.schema("Category", schema=CategorySchema)
+    category = fields.Nested(
+        CategorySchema,
+        description="A category",
+        invalid_property="not in the result",
+        x_extension="A great extension",
+    )
+    result = spec_fixture.openapi.field2property(category)
+    assert result == {
+        "allOf": [build_ref(spec_fixture.spec, "schema", "Category")],
+        "description": "A category",
+        "x-extension": "A great extension",
+    }
+
+
+def test_field2property_nested_spec(spec_fixture):
+    spec_fixture.spec.components.schema("Category", schema=CategorySchema)
+    category = fields.Nested(CategorySchema)
+    assert spec_fixture.openapi.field2property(category) == build_ref(
+        spec_fixture.spec, "schema", "Category"
+    )
+
+
+def test_field2property_nested_many_spec(spec_fixture):
+    spec_fixture.spec.components.schema("Category", schema=CategorySchema)
+    category = fields.Nested(CategorySchema, many=True)
+    ret = spec_fixture.openapi.field2property(category)
+    assert ret["type"] == "array"
+    assert ret["items"] == build_ref(spec_fixture.spec, "schema", "Category")
+
+
+def test_field2property_nested_ref(spec_fixture):
+    category = fields.Nested(CategorySchema)
+    ref = spec_fixture.openapi.field2property(category)
+    assert ref == build_ref(spec_fixture.spec, "schema", "Category")
+
+
+def test_field2property_nested_many(spec_fixture):
+    categories = fields.Nested(CategorySchema, many=True)
+    res = spec_fixture.openapi.field2property(categories)
+    assert res["type"] == "array"
+    assert res["items"] == build_ref(spec_fixture.spec, "schema", "Category")
+
+
+def test_nested_field_with_property(spec_fixture):
+    category_1 = fields.Nested(CategorySchema)
+    category_2 = fields.Nested(CategorySchema, dump_only=True)
+    category_3 = fields.Nested(CategorySchema, many=True)
+    category_4 = fields.Nested(CategorySchema, many=True, dump_only=True)
+    spec_fixture.spec.components.schema("Category", schema=CategorySchema)
+
+    assert spec_fixture.openapi.field2property(category_1) == build_ref(
+        spec_fixture.spec, "schema", "Category"
+    )
+    assert spec_fixture.openapi.field2property(category_2) == {
+        "allOf": [build_ref(spec_fixture.spec, "schema", "Category")],
+        "readOnly": True,
+    }
+    assert spec_fixture.openapi.field2property(category_3) == {
+        "items": build_ref(spec_fixture.spec, "schema", "Category"),
+        "type": "array",
+    }
+    assert spec_fixture.openapi.field2property(category_4) == {
+        "items": build_ref(spec_fixture.spec, "schema", "Category"),
+        "readOnly": True,
+        "type": "array",
+    }
+
+
+def test_custom_properties_for_custom_fields():
+    def custom_string2properties(field, **kwargs):
+        ret = {}
+        if isinstance(field, CustomStringField):
+            ret["x-customString"] = True
+        return ret
+
+    ma_plugin = MarshmallowPlugin()
+    APISpec(
+        title="Validation", version="0.1", openapi_version="3.0.0", plugins=(ma_plugin,)
+    )
+    ma_plugin.openapi.add_attribute_function(custom_string2properties)
+    properties = ma_plugin.openapi.field2property(CustomStringField())
+    assert properties["x-customString"]
diff --git a/tests/test_ext_marshmallow_openapi.py b/tests/test_ext_marshmallow_openapi.py
index cdce28e..37cab0f 100644
--- a/tests/test_ext_marshmallow_openapi.py
+++ b/tests/test_ext_marshmallow_openapi.py
@@ -1,7 +1,4 @@
 # -*- coding: utf-8 -*-
-
-import re
-
 import pytest
 
 from marshmallow import fields, Schema, validate
@@ -10,120 +7,11 @@ from apispec.ext.marshmallow import MarshmallowPlugin
 from apispec.ext.marshmallow.openapi import MARSHMALLOW_VERSION_INFO
 from apispec import exceptions, utils, APISpec
 
+from .schemas import CustomList, CustomStringField
 from .utils import get_schemas, build_ref
 
 
-class CustomList(fields.List):
-    pass
-
-
-class CustomStringField(fields.String):
-    pass
-
-
-class CustomIntegerField(fields.Integer):
-    pass
-
-
 class TestMarshmallowFieldToOpenAPI:
-    def test_field2choices_preserving_order(self, openapi):
-        choices = ["a", "b", "c", "aa", "0", "cc"]
-        field = fields.String(validate=validate.OneOf(choices))
-        assert openapi.field2choices(field) == {"enum": choices}
-
-    @pytest.mark.parametrize(
-        ("FieldClass", "jsontype"),
-        [
-            (fields.Integer, "integer"),
-            (fields.Number, "number"),
-            (fields.Float, "number"),
-            (fields.String, "string"),
-            (fields.Str, "string"),
-            (fields.Boolean, "boolean"),
-            (fields.Bool, "boolean"),
-            (fields.UUID, "string"),
-            (fields.DateTime, "string"),
-            (fields.Date, "string"),
-            (fields.Time, "string"),
-            (fields.Email, "string"),
-            (fields.URL, "string"),
-            # Assume base Field and Raw are strings
-            (fields.Field, "string"),
-            (fields.Raw, "string"),
-            # Custom fields inherit types from their parents
-            (CustomStringField, "string"),
-            (CustomIntegerField, "integer"),
-        ],
-    )
-    def test_field2property_type(self, FieldClass, jsontype, openapi):
-        field = FieldClass()
-        res = openapi.field2property(field)
-        assert res["type"] == jsontype
-
-    @pytest.mark.parametrize("ListClass", [fields.List, CustomList])
-    def test_formatted_field_translates_to_array(self, ListClass, openapi):
-        field = ListClass(fields.String)
-        res = openapi.field2property(field)
-        assert res["type"] == "array"
-        assert res["items"] == openapi.field2property(fields.String())
-
-    @pytest.mark.parametrize(
-        ("FieldClass", "expected_format"),
-        [
-            (fields.Integer, "int32"),
-            (fields.Float, "float"),
-            (fields.UUID, "uuid"),
-            (fields.DateTime, "date-time"),
-            (fields.Date, "date"),
-            (fields.Email, "email"),
-            (fields.URL, "url"),
-        ],
-    )
-    def test_field2property_formats(self, FieldClass, expected_format, openapi):
-        field = FieldClass()
-        res = openapi.field2property(field)
-        assert res["format"] == expected_format
-
-    def test_field_with_description(self, openapi):
-        field = fields.Str(description="a username")
-        res = openapi.field2property(field)
-        assert res["description"] == "a username"
-
-    def test_field_with_missing(self, openapi):
-        field = fields.Str(default="foo", missing="bar")
-        res = openapi.field2property(field)
-        assert res["default"] == "bar"
-
-    def test_field_with_boolean_false_missing(self, openapi):
-        field = fields.Boolean(default=None, missing=False)
-        res = openapi.field2property(field)
-        assert res["default"] is False
-
-    def test_field_with_missing_load(self, openapi):
-        field = fields.Str(default="foo", missing="bar")
-        res = openapi.field2property(field)
-        assert res["default"] == "bar"
-
-    def test_field_with_boolean_false_missing_load(self, openapi):
-        field = fields.Boolean(default=None, missing=False)
-        res = openapi.field2property(field)
-        assert res["default"] is False
-
-    def test_field_with_missing_callable(self, openapi):
-        field = fields.Str(missing=lambda: "dummy")
-        res = openapi.field2property(field)
-        assert "default" not in res
-
-    def test_field_with_doc_default(self, openapi):
-        field = fields.Str(doc_default="Manual default")
-        res = openapi.field2property(field)
-        assert res["default"] == "Manual default"
-
-    def test_field_with_doc_default_and_missing(self, openapi):
-        field = fields.Int(doc_default=42, missing=12)
-        res = openapi.field2property(field)
-        assert res["default"] == 42
-
     def test_fields_with_missing_load(self, openapi):
         field_dict = {"field": fields.Str(default="foo", missing="bar")}
         res = openapi.fields2parameters(field_dict, default_in="query")
@@ -203,79 +91,6 @@ class TestMarshmallowFieldToOpenAPI:
         res = openapi.schema2parameters(schema=UserSchema, default_in="query")
         assert len(res) == 0
 
-    def test_field_with_choices(self, openapi):
-        field = fields.Str(validate=validate.OneOf(["freddie", "brian", "john"]))
-        res = openapi.field2property(field)
-        assert set(res["enum"]) == {"freddie", "brian", "john"}
-
-    def test_field_with_equal(self, openapi):
-        field = fields.Str(validate=validate.Equal("only choice"))
-        res = openapi.field2property(field)
-        assert res["enum"] == ["only choice"]
-
-    def test_only_allows_valid_properties_in_metadata(self, openapi):
-        field = fields.Str(
-            missing="foo",
-            description="foo",
-            enum=["red", "blue"],
-            allOf=["bar"],
-            not_valid="lol",
-        )
-        res = openapi.field2property(field)
-        assert res["default"] == field.missing
-        assert "description" in res
-        assert "enum" in res
-        assert "allOf" in res
-        assert "not_valid" not in res
-
-    def test_field_with_choices_multiple(self, openapi):
-        field = fields.Str(
-            validate=[
-                validate.OneOf(["freddie", "brian", "john"]),
-                validate.OneOf(["brian", "john", "roger"]),
-            ]
-        )
-        res = openapi.field2property(field)
-        assert set(res["enum"]) == {"brian", "john"}
-
-    def test_field_with_additional_metadata(self, openapi):
-        field = fields.Str(minLength=6, maxLength=100)
-        res = openapi.field2property(field)
-        assert res["maxLength"] == 100
-        assert res["minLength"] == 6
-
-    def test_field_with_allow_none(self, openapi):
-        field = fields.Str(allow_none=True)
-        res = openapi.field2property(field)
-        if openapi.openapi_version.major < 3:
-            assert res["x-nullable"] is True
-        else:
-            assert res["nullable"] is True
-
-    def test_field_with_str_regex(self, openapi):
-        regex_str = "^[a-zA-Z0-9]$"
-        field = fields.Str(validate=validate.Regexp(regex_str))
-        ret = openapi.field2property(field)
-        assert ret["pattern"] == regex_str
-
-    def test_field_with_pattern_obj_regex(self, openapi):
-        regex_str = "^[a-zA-Z0-9]$"
-        field = fields.Str(validate=validate.Regexp(re.compile(regex_str)))
-        ret = openapi.field2property(field)
-        assert ret["pattern"] == regex_str
-
-    def test_field_with_no_pattern(self, openapi):
-        field = fields.Str()
-        ret = openapi.field2property(field)
-        assert "pattern" not in ret
-
-    def test_field_with_multiple_patterns(self, recwarn, openapi):
-        regex_validators = [validate.Regexp("winner"), validate.Regexp("loser")]
-        field = fields.Str(validate=regex_validators)
-        with pytest.warns(UserWarning, match="More than one regex validator"):
-            ret = openapi.field2property(field)
-        assert ret["pattern"] == "winner"
-
 
 class TestMarshmallowSchemaToModelDefinition:
     def test_invalid_schema(self, openapi):
@@ -589,46 +404,6 @@ class PetSchema(Schema):
 
 
 class TestNesting:
-    def test_field2property_nested_spec_metadatas(self, spec_fixture):
-        spec_fixture.spec.components.schema("Category", schema=CategorySchema)
-        category = fields.Nested(
-            CategorySchema,
-            description="A category",
-            invalid_property="not in the result",
-            x_extension="A great extension",
-        )
-        result = spec_fixture.openapi.field2property(category)
-        assert result == {
-            "allOf": [build_ref(spec_fixture.spec, "schema", "Category")],
-            "description": "A category",
-            "x-extension": "A great extension",
-        }
-
-    def test_field2property_nested_spec(self, spec_fixture):
-        spec_fixture.spec.components.schema("Category", schema=CategorySchema)
-        category = fields.Nested(CategorySchema)
-        assert spec_fixture.openapi.field2property(category) == build_ref(
-            spec_fixture.spec, "schema", "Category"
-        )
-
-    def test_field2property_nested_many_spec(self, spec_fixture):
-        spec_fixture.spec.components.schema("Category", schema=CategorySchema)
-        category = fields.Nested(CategorySchema, many=True)
-        ret = spec_fixture.openapi.field2property(category)
-        assert ret["type"] == "array"
-        assert ret["items"] == build_ref(spec_fixture.spec, "schema", "Category")
-
-    def test_field2property_nested_ref(self, spec_fixture):
-        category = fields.Nested(CategorySchema)
-        ref = spec_fixture.openapi.field2property(category)
-        assert ref == build_ref(spec_fixture.spec, "schema", "Category")
-
-    def test_field2property_nested_many(self, spec_fixture):
-        categories = fields.Nested(CategorySchema, many=True)
-        res = spec_fixture.openapi.field2property(categories)
-        assert res["type"] == "array"
-        assert res["items"] == build_ref(spec_fixture.spec, "schema", "Category")
-
     def test_schema2jsonschema_with_nested_fields(self, spec_fixture):
         res = spec_fixture.openapi.schema2jsonschema(PetSchema)
         props = res["properties"]
@@ -687,30 +462,6 @@ class TestNesting:
         category_props = get_schemas(spec)["Category"]["properties"]
         assert "breed" not in category_props
 
-    def test_nested_field_with_property(self, spec_fixture):
-        category_1 = fields.Nested(CategorySchema)
-        category_2 = fields.Nested(CategorySchema, dump_only=True)
-        category_3 = fields.Nested(CategorySchema, many=True)
-        category_4 = fields.Nested(CategorySchema, many=True, dump_only=True)
-        spec_fixture.spec.components.schema("Category", schema=CategorySchema)
-
-        assert spec_fixture.openapi.field2property(category_1) == build_ref(
-            spec_fixture.spec, "schema", "Category"
-        )
-        assert spec_fixture.openapi.field2property(category_2) == {
-            "allOf": [build_ref(spec_fixture.spec, "schema", "Category")],
-            "readOnly": True,
-        }
-        assert spec_fixture.openapi.field2property(category_3) == {
-            "items": build_ref(spec_fixture.spec, "schema", "Category"),
-            "type": "array",
-        }
-        assert spec_fixture.openapi.field2property(category_4) == {
-            "items": build_ref(spec_fixture.spec, "schema", "Category"),
-            "readOnly": True,
-            "type": "array",
-        }
-
 
 def test_openapi_tools_validate_v2():
     ma_plugin = MarshmallowPlugin()