go-restful-openapi
go-restful-openapi copied to clipboard
Response schema for []byte is always generated as array of integer rather than byte/binary string
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: https://github.com/emicklei/go-restful-openapi/issues/77#issuecomment-1005367776
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!
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.
i am investigating whether adding prop.Format = "binary"
on line 319 will fix this
for fields of type []byte
you can put the format in a tag:
type BA struct { ByteArray []byte `format:"binary"` }
``
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 https://github.com/emicklei/go-restful-openapi/issues/77#issuecomment-963870791, 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.
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).
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"
}
}
}
}
}
}
}