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

Response schema for []byte is always generated as array of integer rather than byte/binary string #115

Open
brentwritescode opened this issue Oct 11, 2023 · 6 comments

Comments

@brentwritescode
Copy link

brentwritescode commented Oct 11, 2023

Hello,

I'm currently running into an issue where the Swagger generated for our service definition translates Writes([]byte{}) to an array of integer in the output Swagger spec. This is causing some issues for downstream code generation where clients are incorrectly attempting to use []int rather than []byte when forming requests I saw another user mention the same thing in another issue here: #77 (comment)

To be very specific, going by https://swagger.io/docs/specification/data-models/data-types/#file, I need a way to make this translate to "type": "string" and "format": "binary" (or "byte") in the output Swagger definition. Is there a good way to do that?

I adapted one of your previous examples to show what I mean if it helps. I made a really simple API that just returns a random byte array:

package main

import (
	"crypto/rand"
	"log"
	"net/http"

	restful "github.com/emicklei/go-restful"

	restfulspec "github.com/emicklei/go-restful-openapi"
)

func getRandomBytes(size int) ([]byte, error) {
	b := make([]byte, size)
	_, err := rand.Read(b)
	if err != nil {
		return nil, err
	}
	return b, nil
}

func WebService() *restful.WebService {
	ws := new(restful.WebService)
	ws.Path("/").Produces(restful.MIME_OCTET)

	ws.Route(ws.GET("/bytes").To(getRandomBytesTest).
		Doc("get random bytes").
		Writes([]byte{}).
		Produces(restful.MIME_OCTET).
		Returns(200, "OK", []byte{}))

	return ws
}

func getRandomBytesTest(_ *restful.Request, response *restful.Response) {
	responseBytes, _ := getRandomBytes(256)
	_, _ = response.Write(responseBytes)
}

func main() {
	restful.DefaultContainer.Add(WebService())

	config := restfulspec.Config{
		WebServices: restful.RegisteredWebServices(),
		APIPath:     "/apidocs.json",
	}
	restful.DefaultContainer.Add(restfulspec.NewOpenAPIService(config))

	log.Printf("Get the API using http://localhost:8080/apidocs.json")
	log.Fatal(http.ListenAndServe(":8080", nil))
}

When running that application and visiting http://localhost:8080/apidocs.json, it produces the following Swagger:

{
 "swagger": "2.0",
 "paths": {
  "/bytes": {
   "get": {
    "produces": [
     "application/octet-stream"
    ],
    "summary": "get random bytes",
    "operationId": "getRandomBytesTest",
    "responses": {
     "200": {
      "description": "OK",
      "schema": {
       "type": "array",
       "items": {
        "type": "integer"
       }
      }
     }
    }
   }
  }
 }
}

But what I need is to be able to produce this instead for the response:

"responses": {
     "200": {
      "description": "OK",
      "schema": {
       "type": "string",
       "format": "binary"
      }
     }
    }

Is that something that would be feasible to do with the library? And if so, would you be able to suggest how I might be able to achieve that? Thank you for your time and help!

@emicklei
Copy link
Owner

thank for reporting this and providing the example with expected output. I need to look back at the setup to see what is possible now or what can be done to achieve this.

@emicklei
Copy link
Owner

i am investigating whether adding prop.Format = "binary" on line 319 will fix this

@emicklei
Copy link
Owner

for fields of type []byte you can put the format in a tag:

type BA struct { ByteArray        []byte `format:"binary"` }
``

@brentwritescode
Copy link
Author

Thanks for the details!

I was wondering if there was a struct tag-style way to do it. At the moment, I'm literally returning a []byte as the response (e.g. picture what Amazon S3's GetObject API would return in the HTTP body https://docs.aws.amazon.com/AmazonS3/latest/API/API_GetObject.html). I'll see if I can experiment with the format:"binary" style of doing it to see if it will generate what I want.

We did find a workaround using PostBuildSwaggerObjectHandler from the example demonstrated on #77 (comment), though that effectively means we're still generating "incorrect" Swagger and then going back and editing it afterward which isn't ideal, but seems to work at least.

@brentwritescode
Copy link
Author

brentwritescode commented Oct 13, 2023

As a quick experiment, I tried the previous strategy of using a struct with tags for an embedded field to see what it looks like:

package main

import (
	"crypto/rand"
	"log"
	"net/http"

	restful "github.com/emicklei/go-restful"

	restfulspec "github.com/emicklei/go-restful-openapi"
)

type BinaryResponse struct {
	bytes []byte `format:"binary"`
}

func getRandomBytes(size int) ([]byte, error) {
	b := make([]byte, size)
	_, err := rand.Read(b)
	if err != nil {
		return nil, err
	}
	return b, nil
}

func WebService() *restful.WebService {
	ws := new(restful.WebService)
	ws.Path("/").Produces(restful.MIME_OCTET)

	ws.Route(ws.GET("/bytes").To(getRandomBytesTest).
		Doc("get random bytes").
		Writes(BinaryResponse{}).
		//Produces(restful.MIME_OCTET).
		//Returns(200, "OK", []byte{}))
		Returns(200, "OK", BinaryResponse{}))

	return ws
}

func getRandomBytesTest(_ *restful.Request, response *restful.Response) {
	responseBytes, _ := getRandomBytes(256)
	//_, _ = response.Write(responseBytes)
	byteResponse := BinaryResponse{bytes: responseBytes}
	response.WriteEntity(byteResponse)
}

func main() {
	restful.DefaultContainer.Add(WebService())

	config := restfulspec.Config{
		WebServices: restful.RegisteredWebServices(),
		APIPath:     "/apidocs.json",
	}
	restful.DefaultContainer.Add(restfulspec.NewOpenAPIService(config))

	log.Printf("Get the API using http://localhost:8080/apidocs.json")
	log.Fatal(http.ListenAndServe(":8080", nil))
}

The full output Swagger then comes out to something like this:

{
 "swagger": "2.0",
 "paths": {
  "/bytes": {
   "get": {
    "produces": [
     "application/octet-stream"
    ],
    "summary": "get random bytes",
    "operationId": "getRandomBytesTest",
    "responses": {
     "200": {
      "description": "OK",
      "schema": {
       "$ref": "#/definitions/main.BinaryResponse"
      }
     }
    }
   }
  }
 },
 "definitions": {
  "main.BinaryResponse": {
   "required": [
    "bytes"
   ],
   "properties": {
    "bytes": {
     "type": "string",
     "format": "binary"
    }
   }
  }
 }
}

So it definitely does do the binary property specification as you said. It's not entirely clear to me that in Swagger this means the response should be expected to be raw bytes versus bytes wrapped in a JSON response (though I am not a Swagger expert by any means).

@brentwritescode
Copy link
Author

As a third example, this is what we're doing right now to fix up the Swagger before publishing using the PostBuildSwaggerObjectHandler:

package main

import (
	"crypto/rand"
	"log"
	"net/http"

	"github.com/go-openapi/spec"

	restful "github.com/emicklei/go-restful"

	restfulspec "github.com/emicklei/go-restful-openapi"
)

const (
	testPath = "/bytes"
)

func getRandomBytes(size int) ([]byte, error) {
	b := make([]byte, size)
	_, err := rand.Read(b)
	if err != nil {
		return nil, err
	}
	return b, nil
}

func WebService() *restful.WebService {
	ws := new(restful.WebService)
	ws.Path("/").Produces(restful.MIME_OCTET)

	ws.Route(ws.GET(testPath).To(getRandomBytesTest).
		Doc("get random bytes").
		Writes([]byte{}).
		Produces(restful.MIME_OCTET).
		Returns(200, "OK", []byte{}))

	return ws
}

func getRandomBytesTest(_ *restful.Request, response *restful.Response) {
	responseBytes, _ := getRandomBytes(256)
	_, _ = response.Write(responseBytes)
}

func main() {
	restful.DefaultContainer.Add(WebService())

	config := restfulspec.Config{
		WebServices:                   restful.RegisteredWebServices(),
		APIPath:                       "/apidocs.json",
		PostBuildSwaggerObjectHandler: interceptSwagger,
	}
	restful.DefaultContainer.Add(restfulspec.NewOpenAPIService(config))

	log.Printf("Get the API using http://localhost:8080/apidocs.json")
	log.Fatal(http.ListenAndServe(":8080", nil))
}

func interceptSwagger(s *spec.Swagger) {
	if s.Paths != nil {
		bytesPath := s.Paths.Paths[testPath]
		getObjRes := bytesPath.Get.Responses
		if getObjRes != nil {
			okRes := getObjRes.StatusCodeResponses[http.StatusOK]
			newSchema := new(spec.Schema)
			newSchema.Type = []string{"string"}
			newSchema.Format = "binary"
			okRes.Schema = newSchema
			getObjRes.StatusCodeResponses[http.StatusOK] = okRes
		}
	}
}

And that does produce more of the minimal Swagger we're going for:

{
 "swagger": "2.0",
 "paths": {
  "/bytes": {
   "get": {
    "produces": [
     "application/octet-stream"
    ],
    "summary": "get random bytes",
    "operationId": "getRandomBytesTest",
    "responses": {
     "200": {
      "description": "OK",
      "schema": {
       "type": "string",
       "format": "binary"
      }
     }
    }
   }
  }
 }
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants