Hacktoberfest 2026 : les issues que les mainteneurs ont marquées pour octobre, ouvertes et accessibles aux débutants. Parcourir les issues Hacktoberfest

Recursive components are not pruned correctly

Ouverte
#1,856 0 commentaires 3 réactions 0 personnes assignées Voir sur GitHub

Les mainteneurs répondent en général sous 1 jour

Personne n'a encore pris cette issue.

Évaluation

Difficulté
3/5
Temps estimé
1-2 jours
Accessibilité débutants
50/100
Type d'issue
Bug
Clarté
Clairement spécifiée
Activité
À l'abandon
Stack technique
go
Domaine
devtools

Piste de recherche

The issue is in the pruning logic for unused schemas. Look at the code that traverses and prunes components, likely in the generator's schema resolution or code generation phase. The test case is a recursive type Bar that references itself. Start by understanding how the generator tracks used schemas and why it fails to prune a recursive type that is not referenced by any included operation. Run the provided spec.yaml through the generator to see the problem, then examine the pruning algorithm.

Rédigé par le modèle d'indexation à partir du texte de l'issue.

Description

Hello !

Summary

When generating a client for a huge API I did not own, I noticed that there were a lot of unused types in my generated clients.

Looking into the spec that caused this issues, it seems that the pruning does not detect whether recursive types are used or not.

And I cannot exclude them explicitly since the generator think they are used.

How to reproduce

I want to generate a client for the following file, but without the operation bar_get.

I run oapi-codegen -generate "types,client" -include-operation-ids=foo_get -package test -o ./test.gen.go ./spec.yaml on the following file :

# spec.yaml
openapi: 3.0.0
paths:
  /api/foo:
    get:
      operationId: foo_get
      responses:
        "200": {}
  /api/bar:
    get:
      operationId: bar_get
      responses:
        "200":
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Bar"
components:
  schemas:
    Bar:
      properties:
        recursion:
          $ref: "#/components/schemas/Bar"

The generator correctly don't generate functions for the operation bar_get, but still generates types for the component `bar :

// Bar defines model for Bar.
type Bar struct {
	Recursion *Bar `json:"recursion,omitempty"`
}

// The rest of the client is generated as expected...
Full generated file
// Package test provides primitives to interact with the openapi HTTP API.
//
// Code generated by github.com/oapi-codegen/oapi-codegen/v2 version v2.4.1 DO NOT EDIT.
package test

import (
	"context"
	"fmt"
	"io"
	"net/http"
	"net/url"
	"strings"
)

// Bar defines model for Bar.
type Bar struct {
	Recursion *Bar `json:"recursion,omitempty"`
}

// RequestEditorFn  is the function signature for the RequestEditor callback function
type RequestEditorFn func(ctx context.Context, req *http.Request) error

// Doer performs HTTP requests.
//
// The standard http.Client implements this interface.
type HttpRequestDoer interface {
	Do(req *http.Request) (*http.Response, error)
}

// Client which conforms to the OpenAPI3 specification for this service.
type Client struct {
	// The endpoint of the server conforming to this interface, with scheme,
	// https://api.deepmap.com for example. This can contain a path relative
	// to the server, such as https://api.deepmap.com/dev-test, and all the
	// paths in the swagger spec will be appended to the server.
	Server string

	// Doer for performing requests, typically a *http.Client with any
	// customized settings, such as certificate chains.
	Client HttpRequestDoer

	// A list of callbacks for modifying requests which are generated before sending over
	// the network.
	RequestEditors []RequestEditorFn
}

// ClientOption allows setting custom parameters during construction
type ClientOption func(*Client) error

// Creates a new Client, with reasonable defaults
func NewClient(server string, opts ...ClientOption) (*Client, error) {
	// create a client with sane default values
	client := Client{
		Server: server,
	}
	// mutate client and add all optional params
	for _, o := range opts {
		if err := o(&client); err != nil {
			return nil, err
		}
	}
	// ensure the server URL always has a trailing slash
	if !strings.HasSuffix(client.Server, "/") {
		client.Server += "/"
	}
	// create httpClient, if not already present
	if client.Client == nil {
		client.Client = &http.Client{}
	}
	return &client, nil
}

// WithHTTPClient allows overriding the default Doer, which is
// automatically created using http.Client. This is useful for tests.
func WithHTTPClient(doer HttpRequestDoer) ClientOption {
	return func(c *Client) error {
		c.Client = doer
		return nil
	}
}

// WithRequestEditorFn allows setting up a callback function, which will be
// called right before sending the request. This can be used to mutate the request.
func WithRequestEditorFn(fn RequestEditorFn) ClientOption {
	return func(c *Client) error {
		c.RequestEditors = append(c.RequestEditors, fn)
		return nil
	}
}

// The interface specification for the client above.
type ClientInterface interface {
	// FooGet request
	FooGet(ctx context.Context, reqEditors ...RequestEditorFn) (*http.Response, error)
}

func (c *Client) FooGet(ctx context.Context, reqEditors ...RequestEditorFn) (*http.Response, error) {
	req, err := NewFooGetRequest(c.Server)
	if err != nil {
		return nil, err
	}
	req = req.WithContext(ctx)
	if err := c.applyEditors(ctx, req, reqEditors); err != nil {
		return nil, err
	}
	return c.Client.Do(req)
}

// NewFooGetRequest generates requests for FooGet
func NewFooGetRequest(server string) (*http.Request, error) {
	var err error

	serverURL, err := url.Parse(server)
	if err != nil {
		return nil, err
	}

	operationPath := fmt.Sprintf("/api/foo")
	if operationPath[0] == '/' {
		operationPath = "." + operationPath
	}

	queryURL, err := serverURL.Parse(operationPath)
	if err != nil {
		return nil, err
	}

	req, err := http.NewRequest("GET", queryURL.String(), nil)
	if err != nil {
		return nil, err
	}

	return req, nil
}

func (c *Client) applyEditors(ctx context.Context, req *http.Request, additionalEditors []RequestEditorFn) error {
	for _, r := range c.RequestEditors {
		if err := r(ctx, req); err != nil {
			return err
		}
	}
	for _, r := range additionalEditors {
		if err := r(ctx, req); err != nil {
			return err
		}
	}
	return nil
}

// ClientWithResponses builds on ClientInterface to offer response payloads
type ClientWithResponses struct {
	ClientInterface
}

// NewClientWithResponses creates a new ClientWithResponses, which wraps
// Client with return type handling
func NewClientWithResponses(server string, opts ...ClientOption) (*ClientWithResponses, error) {
	client, err := NewClient(server, opts...)
	if err != nil {
		return nil, err
	}
	return &ClientWithResponses{client}, nil
}

// WithBaseURL overrides the baseURL.
func WithBaseURL(baseURL string) ClientOption {
	return func(c *Client) error {
		newBaseURL, err := url.Parse(baseURL)
		if err != nil {
			return err
		}
		c.Server = newBaseURL.String()
		return nil
	}
}

// ClientWithResponsesInterface is the interface specification for the client with responses above.
type ClientWithResponsesInterface interface {
	// FooGetWithResponse request
	FooGetWithResponse(ctx context.Context, reqEditors ...RequestEditorFn) (*FooGetResponse, error)
}

type FooGetResponse struct {
	Body         []byte
	HTTPResponse *http.Response
}

// Status returns HTTPResponse.Status
func (r FooGetResponse) Status() string {
	if r.HTTPResponse != nil {
		return r.HTTPResponse.Status
	}
	return http.StatusText(0)
}

// StatusCode returns HTTPResponse.StatusCode
func (r FooGetResponse) StatusCode() int {
	if r.HTTPResponse != nil {
		return r.HTTPResponse.StatusCode
	}
	return 0
}

// FooGetWithResponse request returning *FooGetResponse
func (c *ClientWithResponses) FooGetWithResponse(ctx context.Context, reqEditors ...RequestEditorFn) (*FooGetResponse, error) {
	rsp, err := c.FooGet(ctx, reqEditors...)
	if err != nil {
		return nil, err
	}
	return ParseFooGetResponse(rsp)
}

// ParseFooGetResponse parses an HTTP response from a FooGetWithResponse call
func ParseFooGetResponse(rsp *http.Response) (*FooGetResponse, error) {
	bodyBytes, err := io.ReadAll(rsp.Body)
	defer func() { _ = rsp.Body.Close() }()
	if err != nil {
		return nil, err
	}

	response := &FooGetResponse{
		Body:         bodyBytes,
		HTTPResponse: rsp,
	}

	return response, nil
}

If I break the recursion by replacing the ref with type: object the generator correctly detect it should prune bar.

Langage dominant
Go
Étoiles
8.6k
Forks
1.1k
Merge moyen
14 h 45 min
PR mergées (30 j)
29

Préparer son environnement

Par où commencer

  1. Lisez l'issue en entier, puis le guide de contribution du projet.
  2. Signalez en commentaire que vous la prenez — cela évite que deux personnes fassent le même travail.
  3. Forkez le dépôt et travaillez sur une branche.
  4. Ouvrez une pull request qui référence le numéro de l'issue.

Autres issues de oapi-codegen/oapi-codegen

Toutes les issues de oapi-codegen/oapi-codegen

Issues similaires

Plus d'issues Go

Recevez les nouvelles issues par e-mail

Un résumé court des issues GitHub adaptées aux débutants.