redocly-cli icon indicating copy to clipboard operation
redocly-cli copied to clipboard

remove-unused-components removes parameters that are in use

Open ethanmdavidson opened this issue 3 months ago • 12 comments

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.

  1. Create redocly.yaml with remove-unused-components: on
  2. generate bundled spec with redocly bundle foo@latest --output=bundled.yml
  3. 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

ethanmdavidson avatar Oct 07 '25 13:10 ethanmdavidson

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.

AlbinaBlazhko17 avatar Oct 13 '25 12:10 AlbinaBlazhko17

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 avatar Oct 17 '25 12:10 ethanmdavidson

@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 avatar Oct 20 '25 13:10 AlbinaBlazhko17

@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)

Image

This behaviour appears to be identical in 2.1.3

nickcorby avatar Oct 22 '25 02:10 nickcorby

@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 avatar Oct 22 '25 08:10 AlbinaBlazhko17

@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 avatar Oct 22 '25 09:10 nickcorby

@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 avatar Oct 22 '25 10:10 AlbinaBlazhko17

@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 avatar Oct 22 '25 10:10 nickcorby

@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).

AlbinaBlazhko17 avatar Oct 22 '25 14:10 AlbinaBlazhko17

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.

ethanmdavidson avatar Oct 22 '25 16:10 ethanmdavidson

@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

nickcorby avatar Oct 22 '25 23:10 nickcorby

@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:

  1. _index.yml contains both schemas ProgramProductResponse and ProgramProduct.
  2. Inside the ProgramProductResponse you have reference to the ProgramProduct and this is creating circular dependency.
  3. When it tries to resolve ProgramProductResponse in _index.yml it goes inside ProgramProductResponse.yml and sees reference back to the _index.yml to the ProgramProduct.

AlbinaBlazhko17 avatar Oct 31 '25 09:10 AlbinaBlazhko17