Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Upgrade from v0.121.0 to v0.122.0 Introduces loss of T.Paths when marshalling to yaml #883

Closed
mcblair opened this issue Dec 7, 2023 · 10 comments
Labels

Comments

@mcblair
Copy link

mcblair commented Dec 7, 2023

Details here: #763 (comment)

@fenollp fenollp added the bug label Dec 8, 2023
fenollp added a commit to fenollp/kin-openapi that referenced this issue Dec 8, 2023
Signed-off-by: Pierre Fenoll <pierrefenoll@gmail.com>
@fenollp
Copy link
Collaborator

fenollp commented Dec 8, 2023

Hey I'm having trouble understanding your issue. Could you tweak #887 to show the bug please? Comment on it or make a PR to it...

@mcblair
Copy link
Author

mcblair commented Dec 8, 2023

Thanks for whipping up that test for this! Super helpful, here is a reproducible example:

package openapi3_test

import (
	"os"
	"testing"

	"github.com/getkin/kin-openapi/openapi3"
	invopopYaml "github.com/invopop/yaml"
	"github.com/stretchr/testify/require"
	"gopkg.in/yaml.v2"
	v3 "gopkg.in/yaml.v3"
)

func TestIssue883(t *testing.T) {
	spec := `
openapi: '3.0.0'
info:
  version: '1.0.0'
  title: Swagger Petstore
  license:
    name: MIT
servers:
- url: http://petstore.swagger.io/v1
paths:
  /simple:
    get:
      summary: A simple GET request
      responses:
        '200':
          description: OK
`[1:]

	sl := openapi3.NewLoader()
	doc, err := sl.LoadFromData([]byte(spec))
	require.NoError(t, err)
	require.NotNil(t, doc.Paths)

	err = doc.Validate(sl.Context)
	require.NoError(t, err)
	require.NotNil(t, doc.Paths)

	t.Run("Roundtrip yaml.v2", func(t *testing.T) {
		// Test marshaling and unmarshaling with yaml.v2
		testYAML(t, doc, spec, yaml.Marshal, unmarshalYAMLv2)
	})

	t.Run("Roundtrip yaml.v3", func(t *testing.T) {
		// Test marshaling and unmarshaling with yaml.v3
		testYAML(t, doc, spec, v3.Marshal, unmarshalYAMLv3)
	})

	t.Run("Roundtrip invopop/yaml", func(t *testing.T) {
		// Test marshaling and unmarshaling with invopop/yaml
		testYAML(t, doc, spec, invopopYaml.Marshal, unmarshalInvopopYAML)
	})
}

func testYAML(t *testing.T, doc *openapi3.T, originalSpec string, marshalFunc func(interface{}) ([]byte, error), unmarshalFunc func([]byte, interface{}) error) {
	marshalledYaml, err := marshalFunc(doc)
	require.NoError(t, err)

	// Create a temporary file
	tmpFile, err := os.CreateTemp("", "spec-*.yaml")
	require.NoError(t, err)
	defer os.Remove(tmpFile.Name()) // Clean up the file afterwards

	// Write to the temporary file
	_, err = tmpFile.Write(marshalledYaml)
	require.NoError(t, err)
	require.NoError(t, tmpFile.Close())

	// Read from the temporary file
	roundTripped, err := os.ReadFile(tmpFile.Name())
	require.NoError(t, err)

	// Unmarshal using the provided function
	var newDoc openapi3.T
	err = unmarshalFunc(roundTripped, &newDoc)
	require.NoError(t, err)
	require.NotNil(t, newDoc.Paths)

	// Compare original and round-tripped specs
	require.YAMLEq(t, originalSpec, string(roundTripped))
}

func unmarshalYAMLv2(data []byte, v interface{}) error {
	return yaml.Unmarshal(data, v)
}

func unmarshalYAMLv3(data []byte, v interface{}) error {
	return v3.Unmarshal(data, v)
}

func unmarshalInvopopYAML(data []byte, v interface{}) error {
	return invopopYaml.Unmarshal(data, v)
}

I can confirm it is reproducible using yaml.v2 and yaml.v3 and not reproducible using "github.com/invopop/yaml".

@mcblair
Copy link
Author

mcblair commented Dec 8, 2023

Checking out v0.121.0 of kin-openapi and running those tests yields all 3 passing.

@fenollp
Copy link
Collaborator

fenollp commented Dec 11, 2023

I see. The thing is yaml v2/v3 packages don't seem to use MarshalYAML => https://pkg.go.dev/gopkg.in/yaml.v3#Marshal

This can indeed be seen as a regression: by introducing private fields (maps in Paths, Callbacks, Responses)

Struct fields are only marshalled if they are exported

Changing to https://github.com/invopop/yaml fixes this, while still depending on yaml.v3.
Is this closure enough for you? Or is it impossible in your context to make the switch?

@mcblair
Copy link
Author

mcblair commented Dec 29, 2023

I see. The thing is yaml v2/v3 packages don't seem to use MarshalYAML => https://pkg.go.dev/gopkg.in/yaml.v3#Marshal

This can indeed be seen as a regression: by introducing private fields (maps in Paths, Callbacks, Responses)

Struct fields are only marshalled if they are exported

Changing to https://github.com/invopop/yaml fixes this, while still depending on yaml.v3. Is this closure enough for you? Or is it impossible in your context to make the switch?

Apologies for the delay, spent a few weeks in the Philippines for the holidays 🇵🇭

We decided to switch entirely to invopop/yaml but then stumbled upon a new issue: invopop/yaml#12 (comment).

Kind of crazy: go with canonical go YAML and the billion-dollar mistake… or go with invopop YAML which flips some behaviors upside down, silently.

fenollp added a commit to fenollp/kin-openapi that referenced this issue Jan 22, 2024
Signed-off-by: Pierre Fenoll <pierrefenoll@gmail.com>
@sonu27
Copy link
Contributor

sonu27 commented Feb 2, 2024

Having the same issue, had to update my open API merger after PathItems were made private, but now paths are empty.

package main

import (
	"fmt"
	"github.com/getkin/kin-openapi/openapi3"
	"github.com/rs/zerolog/log"
	"gopkg.in/yaml.v3"
	"os"
	"path/filepath"
)

func main() {
	base := openapi3.T{
		OpenAPI: "3.0.3",
		Info: &openapi3.Info{
			Title:   "Alfred API",
			Version: "1.0",
		},
		Servers: openapi3.Servers{
			&openapi3.Server{
				Description: "Current",
				URL:         "",
			},
		},
		Paths: &openapi3.Paths{},
		Components: &openapi3.Components{
			Schemas: openapi3.Schemas{},
		},
	}

	err := generate(&base, "./openapi.yaml")
	if err != nil {
		log.Fatal().Err(err).Msg("generating openapi spec")
	}
}

func generate(base *openapi3.T, path string) error {
	files, err := filepath.Glob("./svc/api-*/openapi.yaml")
	if err != nil {
		return fmt.Errorf("finding files: %w", err)
	}

	loader := openapi3.NewLoader()

	for _, file := range files {
		fmt.Printf("processing file: %s\n", file)
		doc, err := loader.LoadFromFile(file)
		if err != nil {
			return fmt.Errorf("loading file: %w", err)
		}

		// loop through paths and add to base, checking for duplicates
		for key, pathItem := range doc.Paths.Map() {
			if base.Paths.Find(key) != nil {
				return fmt.Errorf("duplicate path found: %s", key)
			}
			base.Paths.Set(key, pathItem)
		}

		// loop through schemas and add to base, checking for duplicates
		for name, schema := range doc.Components.Schemas {
			if _, ok := base.Components.Schemas[name]; ok {
				return fmt.Errorf("duplicate schema found: %s", name)
			}
			base.Components.Schemas[name] = schema
		}
	}

	b, err := yaml.Marshal(&base)
	if err != nil {
		return fmt.Errorf("marshalling base: %w", err)
	}

	f, err := os.Create(path)
	if err != nil {
		return fmt.Errorf("file not created: %w", err)
	}

	_, err = f.Write(b)
	if err != nil {
		return fmt.Errorf("writing file: %w", err)
	}
	fmt.Printf("file created: %s\n", path)

	return nil
}

@fenollp
Copy link
Collaborator

fenollp commented Feb 3, 2024

Please @sonu27 use the YAML lib this lib uses: invopop's.

I'm closing this issue and suggest #849 to discuss a different yaml lib.

This current issue is resolved: #883 (comment)

@fenollp fenollp closed this as completed Feb 3, 2024
@sonu27
Copy link
Contributor

sonu27 commented Feb 3, 2024

That library has its own issues (see above)

@fenollp
Copy link
Collaborator

fenollp commented Feb 3, 2024

Does it? It ser/de YAML based on JSON tags, as intended.

@sonu27
Copy link
Contributor

sonu27 commented Feb 5, 2024

Ah fair enough :)
Just switched to it. Will add a comment here: #849

fenollp added a commit to fenollp/kin-openapi that referenced this issue Apr 6, 2024
Signed-off-by: Pierre Fenoll <pierrefenoll@gmail.com>
fenollp added a commit to fenollp/kin-openapi that referenced this issue Apr 6, 2024
Signed-off-by: Pierre Fenoll <pierrefenoll@gmail.com>
fenollp added a commit that referenced this issue Apr 6, 2024
* repro #883

Signed-off-by: Pierre Fenoll <pierrefenoll@gmail.com>

* try harder

Signed-off-by: Pierre Fenoll <pierrefenoll@gmail.com>

* skip yaml unmarshaler for maplike types for now

Signed-off-by: Pierre Fenoll <pierrefenoll@gmail.com>

* docs.sh

Signed-off-by: Pierre Fenoll <pierrefenoll@gmail.com>

---------

Signed-off-by: Pierre Fenoll <pierrefenoll@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

3 participants