My application will serve an API that is available via JSON and XML. There is currently not a convenient way of doing this in Gin. As it is now, I end up with a bunch of switch-cases.
What do you guys say about supporting (maybe a basic implementation) content negotiation (conneg)[0]?
The most simple API could look something like this:
r.GET("/ping", func(c *gin.Context) {
c.Negotiate(200, Response{foo: "Bar"})
})
The response format is then inferred from the Accept-header.
We could also implement a mechanism to try and infer what to return based on URL parameters (/api/?format=xml/json
) or file endings (api.json/xml
).
Is this something you guys want to move forward with? If so, let me know and I'll implement it.
[0] http://en.wikipedia.org/wiki/Content_negotiation
Comment From: manucorporat
Interesting!
Anyway, /api/?format=xml/json
, api.json/xml
do not look very standard.
But using the Accept header looks interesting.
Idea, we could add a:
c.Render(code, binding, data)
it should be used like this:
c.Render(200, binding.JSON, data)
and then add a stric-Accept middleware.
Comment From: pinscript
I'm going out of town for at least a week (vacation :)), so if anyone want to jump in on this, please do.
Comment From: k2xl
Yeah this popped out at me about gin. I guess I could create a middleware encoder that has the negotiation after the .Next, but it seems like this should be something that is done automatically by gin.
Comment From: jmillerdesign
I'm interested in this feature as well. It would be nice to have the control to manipulate the response for each format as well.
Comment From: austinheap
:+1: to @alexandernyquist's request for content negotiation
Comment From: manucorporat
I have a proposal for Content Negociation in Gin:
func (c *Context) NegotiatedFormat() string {
if c.negotiatedFormat != "" {
// Evaluate Accept header
c.negotiatedFormat = "application/json" or "application/xml" or "text/html" ...
}
return c.negotiatedFormat
}
This method is lazily initialized, so the performance will not be affected in the current implementation. It represents the default content negotiation policy but it can be changed with a middleware by calling:
func (c *Context) SetNegotiatedFormat(format string) {
c.negotiatedFormat = format
}
Comment From: manucorporat
An API example:
func main() {
r := gin.Default()
r.GET("/hola", func(c *gin.Context) {
data := gin.H{"status": "ok"}
switch c.NegotiateFormat(gin.MIMEHTML, gin.MIMEJSON) {
case gin.MIMEHTML:
c.HTML(200, "resources/hola.tmpl", data)
case gin.MIMEJSON:
c.JSON(200, data)
}
})
}
by default, gin parses the Accept
header.
If you want to change the behaviour, just add a middleware:
r.Use(func(c *gin.Context) {
var format struct {
Format `form:"format"`
}
c.Bind(&format)
switch format.Format {
case "xml":
c.SetNegotiatedFormat(gin.MIMEXML)
case "json" || "":
c.SetNegotiatedFormat(gin.MIMEJSON)
default:
c.Fail(406, "Not Acceptable")
}
})
resource?format=json
Comment From: Thomasdezeeuw
How about accepting a file extention in the url like /api/resource.json
and /api/resource.xml
?
Comment From: manucorporat
How about accepting a file extention in the url like /api/resource.json and /api/resource.xml?
Two ideas: 1. Using params
func main() {
r := gin.Default()
r.Use(func(c *gin.Context) {
extension := c.Params.ByName("ext")
switch extension {
case "json":
c.SetNegotiatedFormat(gin.MIMEJSON)
case "xml":
c.SetNegotiatedFormat(gin.MIMEJSON)
default:
c.Fail(400, "unknown extension")
}
})
r.GET("/resource.:ext", func(c *gin.Context) {
data := gin.H{"status": "ok"}
switch c.NegotiateFormat(gin.MIMEJSON, gin.MIMEXML) {
case gin.MIMEXML:
c.XML(200, data)
case gin.MIMEJSON:
c.JSON(200, data)
}
})
}
- Using several routes and inspecting the extension:
package main
import "fmt"
import "github.com/gin-gonic/gin"
import "path/filepath"
func main() {
r := gin.Default()
// Create a route group, so this middleware is just applied to this group
negotiation := r.Group("/", func(c *gin.Context) {
switch filepath.Ext(c.Request.URL.Path); {
case "json" || "":
c.SetNegotiatedFormat(gin.MIMEJSON)
case "xml":
c.SetNegotiatedFormat(gin.MIMEJSON)
default:
c.Fail(400, "unknown extension")
}
})
negotiation.GET("/hola.json", resourceHandler)
negotiation.GET("/hola.xml", resourceHandler)
r.Run(":8080")
}
func resourceHandler(c *gin.Context) {
switch c.NegotiateFormat(gin.MIMEJSON, gin.MIMEXML) {
case gin.MIMEXML:
c.XML(200, data)
case gin.MIMEJSON:
c.JSON(200, gin.H{"status": "ok"})
}
}
Comment From: manucorporat
I am also testing a new API:
c.Negotiate(200, gin.H{
"html.file": "resouces/resource.tmpl",
"xml.data": xmlData,
"*.data": jsonData,
})
Comment From: manucorporat
Content.Negotiate() 1. Calls c.NegotiateFormat() internally 2. Based in the config map, it renders HTML, XML or JSON in a efficient way.
This is extremely flexible, since you can:
1. Change the default HTML render, using engine.HTMLRender = render
2. You can change the negotiation algorithm as explained previously using middlewares.
3. It doesn't add performance overhead
4. Short, imperative and powerful API
Comment From: manucorporat
An update:
c.Negotiate(200, gin.Negotiate{
Offered: []string{gin.MIMEJSON, gin.MIMEXML},
Data: jsonData,
XMLData: xmlData,
})
Comment From: manucorporat
https://github.com/gin-gonic/gin/blob/275bdc194ed699776c960e33a80959d1c2ea9570/context.go#L281-L338
Comment From: phisco
I think it could be useful to allow extensions to the Negotiate method, because for example the default being an error could not be the best option for everyone, but as it is it is not possible to modify it, without modifying the library code.
Comment From: jarrodhroberson
Is there a way to register a renderer for a custom MediaType?
I want to use something like application/vnd.myapp.person.json;version=1.0.0
as the "preferred" json format and application/json
as a fallback.
Comment From: Athosone
@jarrodhroberson did you find a way to achieve mediatype versioning?
Comment From: arthurlaveau
@Athosone about you ? Did you find a way to achieve it ?
Comment From: Athosone
@arthurlaveau I did,
I wrote a library to solve the issue. It works with the base go net package. It works natively with gorilla mux and go-chi. it could work with gin gonic with a little effort. If you want to contribute feel free Check the example: https://github.com/Athosone/golib/tree/main/examples/media-type-versioning
Comment From: arthurlaveau
@Athosone thank you for your answer. I will take a look at it!