Repository: marshmallow-code/apispec

Issue:
Sample does not reference components
When I run the sample from https://github.com/marshmallow-code/apispec/blob/dev/docs/quickstart.rst

the expected output

```
# {'components': {'parameters': {},
#                 'responses': {},
#                 'schemas': {'Gist': {'properties': {'id': {'format': 'int64',
#                                                            'type': 'integer'},
#                                                     'name': {'type': 'string'}}}}},
#  'info': {'description': 'A minimal gist API',
#           'title': 'Gisty',
#           'version': '1.0.0'},
#  'openapi': '3.0.2',
#  'paths': OrderedDict([('/gist/{gist_id}',
#                         {'get': {'responses': {'200': {'content': {'application/json': {'schema': {'$ref': '#/definitions/Gist'}}}}}}})]),
#  'tags': []}
```

includes an additional `OrderedDict` (see `responses`) and does not set `$ref` to the schema:

```
{'components': {'schemas': {'Gist': {'properties': {'id': {'format': 'int64',
                                                           'type': 'integer'},
                                                    'name': {'type': 'string'}}}}},
 'info': {'description': 'A minimal gist API',
          'title': 'Gisty',
          'version': '1.0.0'},
 'openapi': '3.0.2',
 'paths': OrderedDict([('/gist/{gist_id}',
                        {'get': {'responses': OrderedDict([('200',
                                                            {'content': {'application/json': {'schema': 'Gist'}}})])}})])}
```
(version 3.3.0)

Discussion:
lafrech: I had to modify the `paths` used in the tests because it uses a schema passed as string and that would mess up with existing tests. Besides, it is not valid OAS 3.
lafrech: I don't know if this should be considered a feature or a bugfix.

It fixes the code to match the doc, but it's basically adding a feature that wasn't there at all, although shown in one doc example.

I'd make this a minor release, not bugfix release.
lafrech: Considering #565, I'm tempted to modify marshmallow plugin as written above. We can make it a major release.
lafrech: I just added a commit to implement the behaviour described above in MarmallowPlugin: if a string does not resolve to a schema from the registry, consider it a reference.

Breaking changes:

- We don't raise if a schema name is typoed, the error is silently ignored and a false ref is generated.
- Registry errors are not caught and turned into ValueError anymore (e.g. if Component.schema is given a string that is not found in the registry). I don't think it is that bad. The error message was not that helpful and the error can be tracked down with the raw RegistryError.

Patch:
diff --git a/src/apispec/core.py b/src/apispec/core.py
index 8c6bb6c..11a7c88 100644
--- a/src/apispec/core.py
+++ b/src/apispec/core.py
@@ -300,13 +300,26 @@ class APISpec:
         Otherwise, it is assumed to be a reference name as string and the corresponding $ref
         string is returned.
 
-        :param str obj_type: "parameter" or "response"
-        :param dict|str obj: parameter or response in dict form or as ref_id string
+        :param str obj_type: "schema", "parameter", "response" or "security_scheme"
+        :param dict|str obj: object in dict form or as ref_id string
         """
         if isinstance(obj, dict):
             return obj
         return build_reference(obj_type, self.openapi_version.major, obj)
 
+    def _resolve_schema(self, obj):
+        """Replace schema reference as string with a $ref if needed."""
+        if not isinstance(obj, dict):
+            return
+        if self.openapi_version.major < 3:
+            if "schema" in obj:
+                obj["schema"] = self.get_ref("schema", obj["schema"])
+        else:
+            if "content" in obj:
+                for content in obj["content"].values():
+                    if "schema" in content:
+                        content["schema"] = self.get_ref("schema", content["schema"])
+
     def clean_parameters(self, parameters):
         """Ensure that all parameters with "in" equal to "path" are also required
         as required by the OpenAPI specification, as well as normalizing any
@@ -365,6 +378,9 @@ class APISpec:
         for operation in (operations or {}).values():
             if "parameters" in operation:
                 operation["parameters"] = self.clean_parameters(operation["parameters"])
+            # OAS 3
+            if "requestBody" in operation:
+                self._resolve_schema(operation["requestBody"])
             if "responses" in operation:
                 responses = OrderedDict()
                 for code, response in operation["responses"].items():
@@ -373,6 +389,6 @@ class APISpec:
                     except (TypeError, ValueError):
                         if self.openapi_version.major < 3 and code != "default":
                             warnings.warn("Non-integer code not allowed in OpenAPI < 3")
-
+                    self._resolve_schema(response)
                     responses[str(code)] = self.get_ref("response", response)
                 operation["responses"] = responses
diff --git a/src/apispec/ext/marshmallow/common.py b/src/apispec/ext/marshmallow/common.py
index 940292b..a781da5 100644
--- a/src/apispec/ext/marshmallow/common.py
+++ b/src/apispec/ext/marshmallow/common.py
@@ -20,13 +20,7 @@ def resolve_schema_instance(schema):
         return schema()
     if isinstance(schema, marshmallow.Schema):
         return schema
-    try:
-        return marshmallow.class_registry.get_class(schema)()
-    except marshmallow.exceptions.RegistryError:
-        raise ValueError(
-            "{!r} is not a marshmallow.Schema subclass or instance and has not"
-            " been registered in the marshmallow class registry.".format(schema)
-        )
+    return marshmallow.class_registry.get_class(schema)()
 
 
 def resolve_schema_cls(schema):
@@ -39,13 +33,7 @@ def resolve_schema_cls(schema):
         return schema
     if isinstance(schema, marshmallow.Schema):
         return type(schema)
-    try:
-        return marshmallow.class_registry.get_class(schema)
-    except marshmallow.exceptions.RegistryError:
-        raise ValueError(
-            "{!r} is not a marshmallow.Schema subclass or instance and has not"
-            " been registered in the marshmallow class registry.".format(schema)
-        )
+    return marshmallow.class_registry.get_class(schema)
 
 
 def get_fields(schema, *, exclude_dump_only=False):
diff --git a/src/apispec/ext/marshmallow/openapi.py b/src/apispec/ext/marshmallow/openapi.py
index f36bfa6..24d77ac 100644
--- a/src/apispec/ext/marshmallow/openapi.py
+++ b/src/apispec/ext/marshmallow/openapi.py
@@ -67,7 +67,12 @@ class OpenAPIConverter(FieldConverterMixin):
 
         :param schema: schema to add to the spec
         """
-        schema_instance = resolve_schema_instance(schema)
+        try:
+            schema_instance = resolve_schema_instance(schema)
+        # If schema is a string and is not found in registry,
+        # assume it is a schema reference
+        except marshmallow.exceptions.RegistryError:
+            return build_reference("schema", self.openapi_version.major, schema)
         schema_key = make_schema_key(schema_instance)
         if schema_key not in self.refs:
             name = self.schema_name_resolver(schema)
diff --git a/src/apispec/ext/marshmallow/schema_resolver.py b/src/apispec/ext/marshmallow/schema_resolver.py
index d76f8c7..07da37b 100644
--- a/src/apispec/ext/marshmallow/schema_resolver.py
+++ b/src/apispec/ext/marshmallow/schema_resolver.py
@@ -162,8 +162,8 @@ class SchemaResolver:
 
     def resolve_schema_dict(self, schema):
         """Resolve a marshmallow Schema class, object, or a string that resolves
-        to a Schema class or an OpenAPI Schema Object containing one of the above
-        to an OpenAPI Schema Object or Reference Object.
+        to a Schema class or a schema reference or an OpenAPI Schema Object
+        containing one of the above to an OpenAPI Schema Object or Reference Object.
 
         If the input is a marshmallow Schema class, object or a string that resolves
         to a Schema class the Schema will be translated to an OpenAPI Schema Object


Test patch:
diff --git a/tests/test_core.py b/tests/test_core.py
index aa5de60..922c739 100644
--- a/tests/test_core.py
+++ b/tests/test_core.py
@@ -307,7 +307,7 @@ class TestPath:
                     }
                 ],
                 "responses": {
-                    "200": {"schema": "Pet", "description": "successful operation"},
+                    "200": {"description": "successful operation"},
                     "400": {"description": "Invalid ID supplied"},
                     "404": {"description": "Pet not found"},
                 },
@@ -618,6 +618,35 @@ class TestPath:
         with pytest.raises(APISpecError, match=message):
             spec.path("/pet/{petId}", operations={"dummy": {}})
 
+    def test_path_resolve_response_schema(self, spec):
+        schema = {"schema": "PetSchema"}
+        if spec.openapi_version.major >= 3:
+            schema = {"content": {"application/json": schema}}
+        spec.path("/pet/{petId}", operations={"get": {"responses": {"200": schema}}})
+        resp = get_paths(spec)["/pet/{petId}"]["get"]["responses"]["200"]
+        if spec.openapi_version.major < 3:
+            schema = resp["schema"]
+        else:
+            schema = resp["content"]["application/json"]["schema"]
+        assert schema == build_ref(spec, "schema", "PetSchema")
+
+    # requestBody only exists in OAS 3
+    @pytest.mark.parametrize("spec", ("3.0.0",), indirect=True)
+    def test_path_resolve_request_body(self, spec):
+        spec.path(
+            "/pet/{petId}",
+            operations={
+                "get": {
+                    "requestBody": {
+                        "content": {"application/json": {"schema": "PetSchema"}}
+                    }
+                }
+            },
+        )
+        assert get_paths(spec)["/pet/{petId}"]["get"]["requestBody"]["content"][
+            "application/json"
+        ]["schema"] == build_ref(spec, "schema", "PetSchema")
+
 
 class TestPlugins:
     @staticmethod
diff --git a/tests/test_ext_marshmallow.py b/tests/test_ext_marshmallow.py
index e6b78a4..72d9b92 100644
--- a/tests/test_ext_marshmallow.py
+++ b/tests/test_ext_marshmallow.py
@@ -822,6 +822,18 @@ class TestOperationHelper:
         json.dumps(data)
         assert get_nested_schema(RunSchema, "sample") is None
 
+    def test_resolve_schema_dict_ref_as_string(self, spec):
+        schema = {"schema": "PetSchema"}
+        if spec.openapi_version.major >= 3:
+            schema = {"content": {"application/json": schema}}
+        spec.path("/pet/{petId}", operations={"get": {"responses": {"200": schema}}})
+        resp = get_paths(spec)["/pet/{petId}"]["get"]["responses"]["200"]
+        if spec.openapi_version.major < 3:
+            schema = resp["schema"]
+        else:
+            schema = resp["content"]["application/json"]["schema"]
+        assert schema == build_ref(spec, "schema", "PetSchema")
+
 
 class TestCircularReference:
     def test_circular_referencing_schemas(self, spec):
diff --git a/tests/test_ext_marshmallow_openapi.py b/tests/test_ext_marshmallow_openapi.py
index 1293543..5879073 100644
--- a/tests/test_ext_marshmallow_openapi.py
+++ b/tests/test_ext_marshmallow_openapi.py
@@ -48,10 +48,6 @@ class TestMarshmallowFieldToOpenAPI:
 
 
 class TestMarshmallowSchemaToModelDefinition:
-    def test_invalid_schema(self, openapi):
-        with pytest.raises(ValueError):
-            openapi.schema2jsonschema(None)
-
     def test_schema2jsonschema_with_explicit_fields(self, openapi):
         class UserSchema(Schema):
             _id = fields.Int()
@@ -200,10 +196,6 @@ class TestMarshmallowSchemaToParameters:
         res = openapi._field2parameter(field, name="field", location="query")
         assert res["required"] is True
 
-    def test_invalid_schema(self, openapi):
-        with pytest.raises(ValueError):
-            openapi.schema2parameters(None, location="json")
-
     # json/body is invalid for OpenAPI 3
     @pytest.mark.parametrize("openapi", ("2.0",), indirect=True)
     def test_schema_body(self, openapi):