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)
        }
    })
}
  1. 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!