Gin: Content negotiation

Created on 8 Jul 2014  路  15Comments  路  Source: gin-gonic/gin

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

enhancement

All 15 comments

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.

I'm going out of town for at least a week (vacation :)), so if anyone want to jump in on this, please do.

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.

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.

:+1: to @alexandernyquist's request for content negotiation

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
}

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

How about accepting a file extention in the url like /api/resource.json and /api/resource.xml?

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"})
    }
}

I am also testing a new API:

    c.Negotiate(200, gin.H{
        "html.file": "resouces/resource.tmpl",
        "xml.data":  xmlData,
        "*.data":    jsonData,
    })

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

An update:

    c.Negotiate(200, gin.Negotiate{
        Offered: []string{gin.MIMEJSON, gin.MIMEXML},
        Data:    jsonData,
        XMLData: xmlData,
    })

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.

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.

Was this page helpful?
0 / 5 - 0 ratings