remove-unused-components removes parameters that are in use
Describe the bug
Adding the remove-unused-components decorator causes the bundle command to generate a bundled spec with no components.parameters included, even though the bundle still has multiple refs to #/components/parameters/foobar. This means that the generated bundle is invalid.
Adding the --dereference option causes all parameters to be pulled into the bundled spec, so I am confident that all the refs are pointing to the correct places. This is not a satisfactory workaround because I don't want a dereferenced spec.
Adding the no-unused-components: error lint doesn't change anything. This linting rule apparently doesn't find any unused components, but the decorator still manages to find 473 "unused" components to remove
To Reproduce Steps to reproduce the behavior:
I haven't been able to create a minimal reproducible example, because it only seems to happen once the spec grows to a certain size, and/or a certain number/structure of refs are used.
- Create redocly.yaml with
remove-unused-components: on - generate bundled spec with
redocly bundle foo@latest --output=bundled.yml - Open bundled.yml and observe that there are no
components.parameters
Expected behavior
All components that are reachable via any number of nested refs should be included in the bundle.
The no-unused-components lint and the remove-unused-components decorator should agree on which components are unused.
OpenAPI description
openapi 3.0.3
Redocly Version(s)
2.3.1
Node.js Version(s)
24.9.0
OS, environment
macos 26.0.1
Additional context
Hi @ethanmdavidson! You can try to add spec-strict-refs rule to your config and lint your file to check if you use refs correctly. Let me know if it helps.
Hi @AlbinaBlazhko17! I do have spec-strict-refs: error in my config and it hasn't flagged anything. I've also manually validated some of the refs to components that are being removed, and haven't found any problems. As far as I can tell, all the refs are correct.
@ethanmdavidson Thank you for letting me know! Could you try downgrading to version 2.1.3 and bundling your file? If that doesn't help, we can have a quick call where you can show me your case. This will help us understand your specific case and resolve the issue. Feel free to contact me or schedule a call at [email protected].
@AlbinaBlazhko17 I'm also experiencing this - on 2.7.0 it looks like it's removing all of the components that are being used, and is leaving the components that aren't (the inverse of what it should be doing)
This behaviour appears to be identical in 2.1.3
@nickcorby Could you please provide an example on how you are using schemas and refs to them? I tested no-unused-components rule and cannot reproduce this behavior.
@AlbinaBlazhko17 I think this might be the cause of the issue. My base.yaml file is structured like so:
openapi: "3.0.2"
info:
title: "Gateway"
version: "v1"
description: "Publicly accessible API"
servers:
- url: "https://au-api.test"
paths:
$ref: endpoints/paths-base.yaml
components:
schemas:
$ref: models/models-base.yaml
paths-base.yaml:
/api/v1/cards:
$ref: api-v1-cards.yaml
models-base.yaml:
200Response:
$ref: 200Response.yaml
ErrorResponse:
$ref: ErrorResponse.yaml
301PresignedURLResponse:
$ref: 301PresignedURLResponse.yaml
When I run the bundle command with the remove-unused-components decorator, all 3 components are removed as unused:
redocly bundle test --remove-unused-components true
bundling test\base.yaml using configuration for api 'test'...
📦 Created a bundle for test\base.yaml at ........ 13ms.
🧹 Removed 3 unused components.
However when I structure base/yaml like so:
openapi: "3.0.2"
info:
title: "Gateway"
version: "v1"
description: "Publicly accessible API"
servers:
- url: "https://au-api.test"
paths:
/api/v1/cards:
$ref: endpoints/api-v1-cards.yaml
components:
schemas:
200Response:
$ref: models/200Response.yaml
ErrorResponse:
$ref: models/ErrorResponse.yaml
301PresignedURLResponse:
$ref: models/301PresignedURLResponse.yaml
Then the bundle command removes only the unused model:
redocly bundle test --remove-unused-components true
bundling test\base.yaml using configuration for api 'test'...
📦 Created a bundle for test\base.yaml at ........ 14ms.
🧹 Removed 1 unused components.
I've structured the base.yaml model like this as we will be adding quite a few paths (over 50 operations for some APIs) & even more models, and want to keep things separated so additions to endpoints & models don't need to touch the base.yaml file. It seems this configuration probably just isn't supported with remove-unused-components.
@nickcorby thank you for providing the example. In your case, in the first example, you are using refs incorrectly by the OpenAPI specification. You can prove this by setting the spec-strict-refs rule to error. That's why, in the first case bundle command removes 3 components, and in the second (valid) case, it removes only one really unused component. I hope, it will help you.
@AlbinaBlazhko17 thanks for getting back to me.
I understand this doesn’t follow OpenAPI specification, however I didn’t think this would be an issue, especially as this structure does work with the bundle command, and the output follows OpenAPI specification (which is the goal).
Maybe this is an unforeseen scenario and not actually supported by the function (or by Redocly), however it’s not immediately clear that the reason it’s not working is because the base.yaml file doesn’t follow OpenAPI specification - this isn’t listed as a requirement anywhere that I can see, unless I missed something?
Regardless, it seems like I need to restructure my repo, unfortunately.
@nickcorby you raised a valid point, we will investigate the issue.
The first approach, how you can solve this issue is to not provide a components.schemas, because bundle command will automatically add all used components:
base.yaml:
openapi: '3.0.2'
info:
title: 'Gateway'
version: 'v1'
description: 'Publicly accessible API'
servers:
- url: 'https://au-api.test'
paths:
$ref: ./paths-base.yaml
paths-base.yaml:
/api/v1/cards:
get:
summary: Get cards
responses:
'200':
$ref: models-base.yaml#/200Response
'400':
$ref: models-base.yaml#/ErrorResponse
'301':
$ref: models-base.yaml#/301PresignedURLResponse
models-base.yaml:
200Response:
description: Successful response
content:
application/json:
schema:
type: string
ErrorResponse:
description: Error response
content:
application/json:
schema:
type: string
301PresignedURLResponse:
description: Presigned URL response
content:
application/json:
schema:
type: string
The second approach, is to run bundle command with output argument (redocly bundle base.yaml --output=output-file), but without remove-unused-components rule and then run bundle on output file with --dereferenced argument (redocly bundle output-file.yaml --dereferenced --remove-unused-components).
Sorry for the slow responses on my end. I don't have much time to spend on this, so I've unfortunately worked around it by simply removing the remove-unused-components decorator. This results in a worse UX in all the things that consume our spec, but we've unfortunately decided that we have more important things to work on. I will try to watch this thread and test out any suggested solutions.
The first approach, how you can solve this issue is to not provide a components.schemas, because bundle command will automatically add all used components:
I tried this approach and ended up with some strange errors:
> redocly bundle spec/unified.yml --output=clients/bundled.yml
bundling spec/unified.yml using configuration for api 'unified@latest'...
[1] spec/unified.yml:226:3 at #/components/parameters/context/schema
Can't resolve $ref
224 | # schemas:
225 | # $ref: 'schemas/_index.yml'
226 | securitySchemes:
227 | JWTAuth:
… | < 444 more lines >
672 | $ref: 'schemas/_index.yml#/DetectConflictsRequestBodyInternalScheduleService'
673 |
Error was generated by the bundler rule.
[2] spec/unified.yml:226:3 at #/components/schemas/ProgramProductResponse/items
Can't resolve $ref
224 | # schemas:
225 | # $ref: 'schemas/_index.yml'
226 | securitySchemes:
227 | JWTAuth:
… | < 444 more lines >
672 | $ref: 'schemas/_index.yml#/DetectConflictsRequestBodyInternalScheduleService'
673 |
Error was generated by the bundler rule.
❌ Errors encountered while bundling spec/unified.yml: bundle not created (use --force to ignore errors).
🧹 Removed 1 unused components.
I don't understand what's going on here. $ref: 'schemas/_index.yml#/DetectConflictsRequestBodyInternalScheduleService' is simply the last line in our root spec file, and isn't even under securitySchemes, it's under components.requestBodies. I suspect there's a bug in the error message, and it's calculating the line numbers from the bundled file, but displaying the original spec.
When I look at the component that the error message specifies (#/components/parameters/context/schema) I do find a ref, and if I manually dereference it (replace it with the content of the referenced file), the error message goes away. However, we have lots of similar components that use refs in the same way, and they aren't causing any issue. Additionally, spec-strict-refs still doesn't flag any errors.
Here are some snippets of our config:
parameters.yml:
context:
name: context
in: query
required: false
schema:
$ref: 'schemas/_index.yml#/Context'
schemas/_index.yml:
Context:
$ref: 'Context.yml'
...
GetProgramProductsResponse:
$ref: 'ProgramProductResponse.yml'
ProgramProduct:
$ref: 'ProgramProduct.yml'
schemas/Context.yml:
type: string
enum:
- admin
- member
- public
schemas/ProgramProductResponse.yml
type: array
items:
$ref: '_index.yml#/ProgramProduct'
The specs are nested several levels in some cases, because we have a large and complex API. However, it's all meeting the openAPI spec afaict, and the spec-strict-refs lint is not complaining.
@AlbinaBlazhko17 removing components/schemas from the base file seems to be working. Thank you for the suggestion!
@ethanmdavidson I reported a bug a few weeks ago where the bundle function wasn't handling certain $ref lines, and I couldn't find any clear reason why - although the bundle command didn't return an error, it just didn't resolve the line. Your issue may be related? https://github.com/Redocly/redocly-cli/issues/2348
Regardless, are you able to get an MVP working that you can then integrate into your current solution? This worked pretty seamlessly for me by just removing the schemas node:
openapi: "3.0.2"
info:
title: "Gateway"
version: "v1"
description: "Publicly accessible API"
servers:
- url: "https://test.api"
paths:
/api/v1/cards:
$ref: endpoints/api-v1-cards.yaml
components:
schemas:
RequestBody:
$ref: models/RequestBody.yaml
200Response:
$ref: models/200Response.yaml
ErrorResponse:
$ref: models/ErrorResponse.yaml
301PresignedURLResponse:
$ref: models/301PresignedURLResponse.yaml
became:
openapi: "3.0.2"
info:
title: "Gateway"
version: "v1"
description: "Publicly accessible API"
servers:
- url: "https://test.api"
paths:
/api/v1/cards:
$ref: endpoints/api-v1-cards.yaml
I've uploaded the repo if you want to take a look: https://github.com/nickcorby/redocly-remove-unused-components/blob/main/base.yaml
@ethanmdavidson Thank you for providing examples, it helps us to allocate what is going wrong.
You need to remove components at all, because bundle command will automatically add all used components. You have already done it, and it caused a first error in your example, error i will fix.
Regarding the second error, which you provided, it caused circular dependency and you need to fix it in your side. Let me explain in detail what is happening:
_index.ymlcontains both schemasProgramProductResponseandProgramProduct.- Inside the
ProgramProductResponseyou have reference to theProgramProductand this is creating circular dependency. - When it tries to resolve
ProgramProductResponsein_index.ymlit goes insideProgramProductResponse.ymland sees reference back to the_index.ymlto theProgramProduct.