Repository: Redocly/redocly-cli
Pull request #897 (2022-10-06 07:57:06)

feat: add media type examples override decorator
## What/Why/How?

Add media type examples override decorator

## Reference
closes: https://github.com/Redocly/redocly-cli/issues/440
## Testing

## Screenshots (optional)

## Check yourself

- [x] Code is linted
- [ ] Tested with redoc/reference-docs/workflows
- [x] All new/updated code is covered with tests

## Security

- [x] Security impact of change has been considered
- [x] Code follows company security practices and guidelines


github-actions[bot]: <!-- jest coverage report action for options with hash 3339ae53a38c44382c0fb4491bdb282d -->

## Coverage report 



| <div title="Status of coverage:&#10; 🟢 - ok&#10; 🟡 - slightly more than threshold&#10; 🔴 - under the threshold">St.<sup>:grey_question:</sup></div> | Category   | Percentage | Covered / Total |
| :----------------------------------------------------------------------------------------------------------------------------------------------------: | :--------- | :--------- | :-------------: |
|                                                                           🟡                                                                           | Statements | 72.91%     |    3270/4485    |
|                                                                           🟡                                                                           | Branches   | 64.69%     |    1823/2818    |
|                                                                           🟡                                                                           | Functions  | 65.35%     |     547/837     |
|                                                                           🟡                                                                           | Lines      | 72.79%     |    3037/4172    |

<details><summary>Show new covered files 🐣</summary>
<br/>

| <div title="Status of coverage:&#10; 🟢 - ok&#10; 🟡 - slightly more than threshold&#10; 🔴 - under the threshold">St.<sup>:grey_question:</sup></div> | File                                                                                                                  | Statements | Branches | Functions | Lines  |
| :----------------------------------------------------------------------------------------------------------------------------------------------------: | :-------------------------------------------------------------------------------------------------------------------- | :--------- | :------- | :-------- | :----- |
|                                                                           🔴                                                                           | <div title="core/src/decorators/common/media-type-examples-override.ts">`...` / media-type-examples-override.ts</div> | 21.05%     | 0%       | 33.33%    | 16.67% |

</details>


<details><summary>Show files with reduced coverage 🔻</summary>
<br/>

| <div title="Status of coverage:&#10; 🟢 - ok&#10; 🟡 - slightly more than threshold&#10; 🔴 - under the threshold">St.<sup>:grey_question:</sup></div> | File              | Statements                                                     | Branches | Functions                                                      | Lines                                                          |
| :----------------------------------------------------------------------------------------------------------------------------------------------------: | :---------------- | :------------------------------------------------------------- | :------- | :------------------------------------------------------------- | :------------------------------------------------------------- |
|                                                                           🟡                                                                           | core/src/utils.ts | <div title="Base coverage is: 74.53%">73.39% (-1.13% 🔻)</div> | 60%      | <div title="Base coverage is: 71.05%">69.23% (-1.82% 🔻)</div> | <div title="Base coverage is: 74.75%">73.53% (-1.22% 🔻)</div> |

</details>

## Test suite run success
506 tests passing in 86 suites.

<p align="right">Report generated by <a href="https://github.com/ArtiomTr/jest-coverage-report-action">🧪jest coverage report action</a> from caaa318f85d144c747fc0d38b0efd2f77a20235b</p>

IgorKarpiuk: > This should always place target examples into the `examples` map and never into `example`. The examples map accepts a value in a specific structure (Example object)[https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#media-type-object] whereas the `example` field expects the raw example itself.
> 
> I didn't see the most common use case tested, which is no examples defined and then the override adds an example to the examples map.

@adamaltman I changed `example` to `examples` now it will be always place target examples into the `examples` field and i added new tests
tatomyr: Left a few minor comments.

Patch:
diff --git a/docs/decorators.md b/docs/decorators.md
index dde1e23e..3a71d114 100644
--- a/docs/decorators.md
+++ b/docs/decorators.md
@@ -18,6 +18,9 @@ They are commonly used to add, remove, or change content.
 - [operation-description-override](./decorators/operation-description-override.md)
 - [tag-description-override](./decorators/tag-description-override.md)
 
+### Changing examples
+- [media-type-examples-override](./decorators/media-type-examples-override.md)
+
 ### Removing content
 
 - [filter-in](./decorators/filter-in.md)
diff --git a/docs/decorators/media-type-examples-override.md b/docs/decorators/media-type-examples-override.md
new file mode 100644
index 00000000..4fdec796
--- /dev/null
+++ b/docs/decorators/media-type-examples-override.md
@@ -0,0 +1,108 @@
+# media-type-examples-override
+
+Replaces the examples in the corresponding file.
+
+## API design principles
+
+Sometimes developers generate OpenAPI and the examples need to be added or improved after the fact.
+This generally happens when you have no permission to edit the source.
+This decorator provides a way to "overlay" a new examples over the source so that as the source changes you won't lose your modifications.
+
+## Configuration
+
+|Option|Type| Description                                                                                                                                                  |
+|---|---|--------------------------------------------------------------------------------------------------------------------------------------------------------------|
+|operationIds|object| **REQUIRED.** Object consisting of operationIds as keys, and object as a value that containing the request and responses keys and example`s paths as values. |
+
+Example of a configuration:
+
+```yaml
+decorators:
+  media-type-examples-override:
+    operationIds:
+      PostPets:
+        request:
+          application/json: ./pet-examples.yaml
+        responses:
+          '200':
+            application/json: ./pet-examples.yaml
+          '400':
+            application/json: ./pet-errors-examples.yaml
+```
+
+## Examples 
+
+Given this definition with example:
+
+```yaml
+openapi: 3.0.0
+info:
+  version: 1.0.0
+  title: Swagger Petstore
+paths:
+  '/GETUser/{userId}':
+    summary: getPet
+    get:
+      operationId: getUserByName
+      responses:
+        200:
+          description: okay
+          content:
+            application/json:
+              examples:
+                pet:
+                  value:
+                    name: test name
+```
+
+Given the file `./pet-examples.yaml` with content:
+
+```yaml
+Cat:
+  value:
+    name: example
+```
+
+Given this configuration:
+
+```yaml
+decorators:
+  media-type-examples-override:
+    operationIds:
+      PostPets:
+        responses:
+          '200': ./pet-examples.yaml
+```
+
+The result of the bundle command
+
+```yaml
+openapi: 3.0.0
+info:
+  version: 1.0.0
+  title: Swagger Petstore
+paths:
+  '/GETUser/{userId}':
+    summary: getPet
+    get:
+      operationId: getUserByName
+      responses:
+        200:
+          description: okay
+          content:
+            application/json:
+              examples:
+                Cat:
+                  value:
+                    name: example
+```
+## Related decorators
+
+- [operation-description-override](./operation-description-override.md)
+- [tag-description-override](./tag-description-override.md)
+- [tag-description-override](./tag-description-override.md)
+
+## Resources
+
+- [Decorator source](https://github.com/Redocly/redocly-cli/blob/main/packages/core/src/decorators/common/info-description-override.ts)
+- [Blog post about Overlays](../../../blog/openapi-overlays.md)
diff --git a/packages/core/src/decorators/common/media-type-examples-override.ts b/packages/core/src/decorators/common/media-type-examples-override.ts
new file mode 100644
index 00000000..97878a68
--- /dev/null
+++ b/packages/core/src/decorators/common/media-type-examples-override.ts
@@ -0,0 +1,79 @@
+import { Oas3Decorator } from '../../visitors';
+import { Oas3Operation, Oas3RequestBody, Oas3Response } from '../../typings/openapi';
+import { yamlAndJsonSyncReader } from '../../utils';
+import { isRef } from '../../ref-utils';
+import { ResolveFn, UserContext } from '../../walk';
+
+export const MediaTypeExamplesOverride: Oas3Decorator = ({ operationIds }) => {
+  return {
+    Operation: {
+      enter(operation: Oas3Operation, ctx: UserContext) {
+        const operationId = operation.operationId;
+
+        if (!operationId) {
+          return;
+        }
+
+        const properties = operationIds[operationId];
+
+        if (!properties) {
+          return;
+        }
+
+        if (properties.responses && operation.responses) {
+          for (const responseCode of Object.keys(properties.responses)) {
+            const resolvedResponse = checkAndResolveRef<Oas3Response>(
+              operation.responses[responseCode],
+              ctx.resolve
+            );
+
+            if (!resolvedResponse) {
+              continue;
+            }
+
+            resolvedResponse.content = resolvedResponse.content ? resolvedResponse.content : {};
+
+            Object.keys(properties.responses[responseCode]).forEach((mimeType) => {
+              resolvedResponse.content![mimeType] = {
+                ...resolvedResponse.content![mimeType],
+                examples: yamlAndJsonSyncReader(properties.responses[responseCode][mimeType]),
+              };
+            });
+
+            operation.responses[responseCode] = resolvedResponse;
+          }
+        }
+
+        if (properties.request && operation.requestBody) {
+          const resolvedRequest = checkAndResolveRef<Oas3RequestBody>(
+            operation.requestBody,
+            ctx.resolve
+          );
+
+          if (!resolvedRequest) {
+            return;
+          }
+
+          resolvedRequest.content = resolvedRequest.content ? resolvedRequest.content : {};
+
+          Object.keys(properties.request).forEach((mimeType) => {
+            resolvedRequest.content[mimeType] = {
+              ...resolvedRequest.content[mimeType],
+              examples: yamlAndJsonSyncReader(properties.request[mimeType]),
+            };
+          });
+          operation.requestBody = resolvedRequest;
+        }
+      },
+    },
+  };
+};
+
+function checkAndResolveRef<T>(node: any, resolver: ResolveFn): T | undefined {
+  if (!isRef(node)) {
+    return node;
+  }
+
+  const resolved = resolver<T>(node);
+  return resolved.error ? undefined : JSON.parse(JSON.stringify(resolved.node));
+}
diff --git a/packages/core/src/decorators/oas3/index.ts b/packages/core/src/decorators/oas3/index.ts
index 2289e522..654f0547 100644
--- a/packages/core/src/decorators/oas3/index.ts
+++ b/packages/core/src/decorators/oas3/index.ts
@@ -6,6 +6,7 @@ import { InfoDescriptionOverride } from '../common/info-description-override';
 import { RemoveXInternal } from '../common/remove-x-internal';
 import { FilterIn } from '../common/filters/filter-in';
 import { FilterOut } from '../common/filters/filter-out';
+import { MediaTypeExamplesOverride } from '../common/media-type-examples-override';
 
 export const decorators = {
   'registry-dependencies': RegistryDependencies as Oas3Decorator,
@@ -15,4 +16,5 @@ export const decorators = {
   'remove-x-internal': RemoveXInternal as Oas3Decorator,
   'filter-in': FilterIn as Oas3Decorator,
   'filter-out': FilterOut as Oas3Decorator,
+  'media-type-examples-override': MediaTypeExamplesOverride as Oas3Decorator,
 };
diff --git a/packages/core/src/utils.ts b/packages/core/src/utils.ts
index 1dd744e8..8b085500 100644
--- a/packages/core/src/utils.ts
+++ b/packages/core/src/utils.ts
@@ -153,6 +153,11 @@ export function readFileAsStringSync(filePath: string) {
   return fs.readFileSync(filePath, 'utf-8');
 }
 
+export function yamlAndJsonSyncReader<T>(filePath: string): T {
+  const content = fs.readFileSync(filePath, 'utf-8');
+  return parseYaml(content) as T;
+}
+
 export function isPathParameter(pathSegment: string) {
   return pathSegment.startsWith('{') && pathSegment.endsWith('}');
 }


Test patch:
diff --git a/packages/core/src/decorators/__tests__/media-type-examples-override.test.ts b/packages/core/src/decorators/__tests__/media-type-examples-override.test.ts
new file mode 100644
index 00000000..91588ded
--- /dev/null
+++ b/packages/core/src/decorators/__tests__/media-type-examples-override.test.ts
@@ -0,0 +1,665 @@
+import { makeConfig, parseYamlToDocument, yamlSerializer } from '../../../__tests__/utils';
+import { outdent } from 'outdent';
+import { bundleDocument } from '../../bundle';
+import { BaseResolver } from '../../resolve';
+
+describe('oas3 media-type-examples-override', () => {
+  expect.addSnapshotSerializer(yamlSerializer);
+  it('should override response example', async () => {
+    const testDocument = parseYamlToDocument(
+      outdent`
+            openapi: 3.0.0
+            paths:
+              /pet:
+                get:
+                  operationId: getUserById
+                  responses:
+                    200:
+                      description: json
+                      content:
+                        application/json:
+                          examples:
+                            def:
+                              value:
+                                a: test
+                      
+    `
+    );
+    const { bundle: res } = await bundleDocument({
+      document: testDocument,
+      externalRefResolver: new BaseResolver(),
+      config: await makeConfig(
+        {},
+        {
+          'media-type-examples-override': {
+            operationIds: {
+              getUserById: {
+                responses: {
+                  '200': {
+                    'application/json':
+                      'packages/core/src/decorators/__tests__/resources/request.yaml',
+                  },
+                },
+              },
+            },
+          },
+        }
+      ),
+    });
+
+    expect(res.parsed).toMatchInlineSnapshot(`
+      openapi: 3.0.0
+      paths:
+        /pet:
+          get:
+            operationId: getUserById
+            responses:
+              '200':
+                description: json
+                content:
+                  application/json:
+                    examples:
+                      def:
+                        value:
+                          b: from external file
+      components: {}
+
+    `);
+  });
+
+  it('should override requestBody examples', async () => {
+    const testDocument = parseYamlToDocument(
+      outdent`
+            openapi: 3.0.0
+            paths:
+              /pet:
+                get:
+                  operationId: getUserById
+                  requestBody: 
+                    content:
+                      application/json:
+                        examples:
+                          def:
+                            value:
+                              a: test123
+                      
+    `
+    );
+    const { bundle: res } = await bundleDocument({
+      document: testDocument,
+      externalRefResolver: new BaseResolver(),
+      config: await makeConfig(
+        {},
+        {
+          'media-type-examples-override': {
+            operationIds: {
+              getUserById: {
+                request: {
+                  'application/json':
+                    'packages/core/src/decorators/__tests__/resources/response.yaml',
+                },
+              },
+            },
+          },
+        }
+      ),
+    });
+
+    expect(res.parsed).toMatchInlineSnapshot(`
+      openapi: 3.0.0
+      paths:
+        /pet:
+          get:
+            operationId: getUserById
+            requestBody:
+              content:
+                application/json:
+                  examples:
+                    def:
+                      value:
+                        name: test response name
+      components: {}
+
+    `);
+  });
+
+  it('should override requestBody examples and 200 response', async () => {
+    const testDocument = parseYamlToDocument(
+      outdent`
+            openapi: 3.0.0
+            paths:
+              /pet:
+                get:
+                  operationId: getUserById
+                  responses:
+                    '200':
+                      description: json
+                      content:
+                        application/json:
+                          examples:
+                            def:
+                              value:
+                                message: test
+                  requestBody: 
+                    content:
+                      application/json:
+                        examples:
+                          def:
+                            value:
+                              a: test123
+                      
+    `
+    );
+    const { bundle: res } = await bundleDocument({
+      document: testDocument,
+      externalRefResolver: new BaseResolver(),
+      config: await makeConfig(
+        {},
+        {
+          'media-type-examples-override': {
+            operationIds: {
+              getUserById: {
+                request: {
+                  'application/json':
+                    'packages/core/src/decorators/__tests__/resources/request.yaml',
+                },
+                responses: {
+                  '200': {
+                    'application/json':
+                      'packages/core/src/decorators/__tests__/resources/response.yaml',
+                  },
+                  '201': {
+                    'application/json':
+                      'packages/core/src/decorators/__tests__/resources/response.yaml',
+                  },
+                },
+              },
+            },
+          },
+        }
+      ),
+    });
+
+    expect(res.parsed).toMatchInlineSnapshot(`
+      openapi: 3.0.0
+      paths:
+        /pet:
+          get:
+            operationId: getUserById
+            responses:
+              '200':
+                description: json
+                content:
+                  application/json:
+                    examples:
+                      def:
+                        value:
+                          name: test response name
+            requestBody:
+              content:
+                application/json:
+                  examples:
+                    def:
+                      value:
+                        b: from external file
+      components: {}
+
+    `);
+  });
+
+  it('should override response and request examples with refs', async () => {
+    const testDocument = parseYamlToDocument(
+      outdent`
+            openapi: 3.0.0
+            paths:
+              /pet:
+                get:
+                  operationId: getUserById
+                  responses:
+                    '200':
+                      $ref: '#/components/responses/okay200'
+                  requestBody: 
+                    $ref: '#/components/requestBodies/testRequest'
+            components:
+              requestBodies:
+                testRequest:
+                  content:
+                    application/json:
+                      examples:
+                        def:
+                          value:
+                            a: test123
+              responses:
+                okay200:
+                  description: json
+                  content:
+                    application/json:
+                      examples:
+                        def:
+                          value:
+                            a: t
+                            b: 3                          
+    `
+    );
+
+    const { bundle: res } = await bundleDocument({
+      document: testDocument,
+      externalRefResolver: new BaseResolver(),
+      config: await makeConfig(
+        {},
+        {
+          'media-type-examples-override': {
+            operationIds: {
+              getUserById: {
+                request: {
+                  'application/json':
+                    'packages/core/src/decorators/__tests__/resources/request.yaml',
+                },
+                responses: {
+                  '200': {
+                    'application/json':
+                      'packages/core/src/decorators/__tests__/resources/response.yaml',
+                  },
+                },
+              },
+            },
+          },
+        }
+      ),
+    });
+
+    expect(res.parsed).toMatchInlineSnapshot(`
+      openapi: 3.0.0
+      paths:
+        /pet:
+          get:
+            operationId: getUserById
+            responses:
+              '200':
+                description: json
+                content:
+                  application/json:
+                    examples:
+                      def:
+                        value:
+                          name: test response name
+            requestBody:
+              content:
+                application/json:
+                  examples:
+                    def:
+                      value:
+                        b: from external file
+      components:
+        requestBodies:
+          testRequest:
+            content:
+              application/json:
+                examples:
+                  def:
+                    value:
+                      a: test123
+        responses:
+          okay200:
+            description: json
+            content:
+              application/json:
+                examples:
+                  def:
+                    value:
+                      a: t
+                      b: 3
+
+    `);
+  });
+
+  it('should override examples with ref', async () => {
+    const testDocument = parseYamlToDocument(
+      outdent`
+            openapi: 3.0.0
+            paths:
+              /pet:
+                get:
+                  operationId: getUserById
+                  responses:
+                    '400':
+                      description: bad request
+                      content:
+                        application/json:
+                          examples:
+                            $ref: '#/components/examples/testExample'
+                  requestBody: 
+                    content:
+                      application/json:
+                        examples:
+                          obj:
+                            $ref: '#/components/examples/testExample'
+            components:
+              examples:
+                testExample:
+                  value:
+                    test: 1                          
+    `
+    );
+
+    const { bundle: res } = await bundleDocument({
+      document: testDocument,
+      externalRefResolver: new BaseResolver(),
+      config: await makeConfig(
+        {},
+        {
+          'media-type-examples-override': {
+            operationIds: {
+              getUserById: {
+                request: {
+                  'application/json':
+                    'packages/core/src/decorators/__tests__/resources/request.yaml',
+                },
+                responses: {
+                  '400': {
+                    'application/json':
+                      'packages/core/src/decorators/__tests__/resources/response.yaml',
+                  },
+                },
+              },
+            },
+          },
+        }
+      ),
+    });
+
+    expect(res.parsed).toMatchInlineSnapshot(`
+      openapi: 3.0.0
+      paths:
+        /pet:
+          get:
+            operationId: getUserById
+            responses:
+              '400':
+                description: bad request
+                content:
+                  application/json:
+                    examples:
+                      def:
+                        value:
+                          name: test response name
+            requestBody:
+              content:
+                application/json:
+                  examples:
+                    def:
+                      value:
+                        b: from external file
+      components:
+        examples:
+          testExample:
+            value:
+              test: 1
+
+    `);
+  });
+
+  it('should add the examples to response and request', async () => {
+    const testDocument = parseYamlToDocument(
+      outdent`
+            openapi: 3.0.0
+            paths:
+              /pet:
+                get:
+                  operationId: getUserById
+                  requestBody:
+                    description: empty body
+                  responses:
+                    '200':
+                      description: json               
+    `
+    );
+    const { bundle: res } = await bundleDocument({
+      document: testDocument,
+      externalRefResolver: new BaseResolver(),
+      config: await makeConfig(
+        {},
+        {
+          'media-type-examples-override': {
+            operationIds: {
+              getUserById: {
+                request: {
+                  'application/json':
+                    'packages/core/src/decorators/__tests__/resources/request.yaml',
+                },
+                responses: {
+                  '200': {
+                    'application/json':
+                      'packages/core/src/decorators/__tests__/resources/response.yaml',
+                  },
+                },
+              },
+            },
+          },
+        }
+      ),
+    });
+
+    expect(res.parsed).toMatchInlineSnapshot(`
+      openapi: 3.0.0
+      paths:
+        /pet:
+          get:
+            operationId: getUserById
+            requestBody:
+              description: empty body
+              content:
+                application/json:
+                  examples:
+                    def:
+                      value:
+                        b: from external file
+            responses:
+              '200':
+                description: json
+                content:
+                  application/json:
+                    examples:
+                      def:
+                        value:
+                          name: test response name
+      components: {}
+
+    `);
+  });
+
+  it('should add examples to response', async () => {
+    const testDocument = parseYamlToDocument(
+      outdent`
+            openapi: 3.0.0
+            paths:
+              /pet:
+                get:
+                  operationId: getUserById
+                  responses:
+                    '200':
+                      description: json
+                      content:
+                        application/json:
+                          schema: 
+                            $ref: "#/components/schemas/Pet"
+    ,               
+    `
+    );
+    const { bundle: res } = await bundleDocument({
+      document: testDocument,
+      externalRefResolver: new BaseResolver(),
+      config: await makeConfig(
+        {},
+        {
+          'media-type-examples-override': {
+            operationIds: {
+              getUserById: {
+                responses: {
+                  '200': {
+                    'application/json':
+                      'packages/core/src/decorators/__tests__/resources/response.yaml',
+                  },
+                },
+              },
+            },
+          },
+        }
+      ),
+    });
+
+    expect(res.parsed).toMatchInlineSnapshot(`
+      openapi: 3.0.0
+      paths:
+        /pet:
+          get:
+            operationId: getUserById
+            responses:
+              '200':
+                description: json
+                content:
+                  application/json:
+                    schema:
+                      $ref: '#/components/schemas/Pet'
+                    examples:
+                      def:
+                        value:
+                          name: test response name
+      components: {}
+
+    `);
+  });
+
+  it('should add examples to requestBody', async () => {
+    const testDocument = parseYamlToDocument(
+      outdent`
+            openapi: 3.0.0
+            paths:
+              /pet:
+                get:
+                  operationId: getUserById
+                  requestBody: 
+                    content:
+                      application/json:
+                        schema: 
+                          $ref: "#/components/schemas/Pet"
+                        
+    ,               
+    `
+    );
+    const { bundle: res } = await bundleDocument({
+      document: testDocument,
+      externalRefResolver: new BaseResolver(),
+      config: await makeConfig(
+        {},
+        {
+          'media-type-examples-override': {
+            operationIds: {
+              getUserById: {
+                request: {
+                  'application/json':
+                    'packages/core/src/decorators/__tests__/resources/response.yaml',
+                  'application/xml':
+                    'packages/core/src/decorators/__tests__/resources/response.yaml',
+                },
+              },
+            },
+          },
+        }
+      ),
+    });
+
+    expect(res.parsed).toMatchInlineSnapshot(`
+      openapi: 3.0.0
+      paths:
+        /pet:
+          get:
+            operationId: getUserById
+            requestBody:
+              content:
+                application/json:
+                  schema:
+                    $ref: '#/components/schemas/Pet'
+                  examples:
+                    def:
+                      value:
+                        name: test response name
+                application/xml:
+                  examples:
+                    def:
+                      value:
+                        name: test response name
+      components: {}
+
+    `);
+  });
+
+  it('should add new examples with new content type to response', async () => {
+    const testDocument = parseYamlToDocument(
+      outdent`
+            openapi: 3.0.0
+            paths:
+              /pet:
+                get:
+                  operationId: getUserById
+                  requestBody:
+                    description: empty body
+                  responses:
+                    '200':
+                      description: json
+                      content:
+                        examples:  
+                          application/json:
+                            value:
+                              name: json type               
+    `
+    );
+
+    const { bundle: res } = await bundleDocument({
+      document: testDocument,
+      externalRefResolver: new BaseResolver(),
+      config: await makeConfig(
+        {},
+        {
+          'media-type-examples-override': {
+            operationIds: {
+              getUserById: {
+                responses: {
+                  '200': {
+                    'application/xml':
+                      'packages/core/src/decorators/__tests__/resources/response.yaml',
+                  },
+                },
+              },
+            },
+          },
+        }
+      ),
+    });
+
+    expect(res.parsed).toMatchInlineSnapshot(`
+      openapi: 3.0.0
+      paths:
+        /pet:
+          get:
+            operationId: getUserById
+            requestBody:
+              description: empty body
+            responses:
+              '200':
+                description: json
+                content:
+                  examples:
+                    application/json:
+                      value:
+                        name: json type
+                  application/xml:
+                    examples:
+                      def:
+                        value:
+                          name: test response name
+      components: {}
+
+    `);
+  });
+});
diff --git a/packages/core/src/decorators/__tests__/resources/request.yaml b/packages/core/src/decorators/__tests__/resources/request.yaml
new file mode 100644
index 00000000..415d1ff1
--- /dev/null
+++ b/packages/core/src/decorators/__tests__/resources/request.yaml
@@ -0,0 +1,3 @@
+def:
+  value:
+    b: from external file
diff --git a/packages/core/src/decorators/__tests__/resources/response.yaml b/packages/core/src/decorators/__tests__/resources/response.yaml
new file mode 100644
index 00000000..da181108
--- /dev/null
+++ b/packages/core/src/decorators/__tests__/resources/response.yaml
@@ -0,0 +1,3 @@
+def:
+  value:
+    name: test response name