Library / SDK
unrolled/render avatar
unrolled/render

Six responses from one Go handler: inside unrolled/render

Go package for easily rendering JSON, XML, binary data, and HTML templates responses.

1,999 stars153 forksGoMIT

At a glance

What is it?
unrolled/render is a Go package that turns one http.ResponseWriter into JSON, JSONP, XML, text, binary and HTML responses through a single Options struct. It wraps three standard packages instead of a web framework, which keeps it small and pushes its oddities into the defaults.
Who is it for?
Adopt unrolled/render when one service answers several content types from the same handler and you want a single Options struct to govern content types, template caching and JSON streaming. Skip it when a single JSON document is all you ever send, since encoding/json plus one header already covers that.
Can I use it commercially?
Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 39 days ago.
What is it written in?
Mainly Go, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 23, 2026, and from our analysis. They are not legal advice.

Editorial analysis

Six render methods over one ResponseWriter

A Go handler can already turn a map into JSON with encoding/json, but every route that wants a different content type repeats the same three steps: choose a header, run an encoder, write bytes to the http.ResponseWriter. unrolled/render bundles those steps into one value built by render.New() and hangs six methods off it, Data, Text, JSON, JSONP, XML and HTML, so a service sets content type behaviour in one place instead of six.

Nothing in that design binds the package to a router. Any handler that receives an http.ResponseWriter qualifies, and the worked example builds on http.NewServeMux() before ending at http.ListenAndServe("127.0.0.1:3000", mux), which means the shape transfers to whatever mux you already run.

Below the surface the work is thin. JSON goes through encoding/json, XML through encoding/xml and HTML through html/template, while Data and Text marshal nothing at all and hand your bytes or string straight to the writer.

go
mux.HandleFunc("/json", func(w http.ResponseWriter, req *http.Request) {
    r.JSON(w, http.StatusOK, map[string]string{"hello": "json"})
})

mux.HandleFunc("/jsonp", func(w http.ResponseWriter, req *http.Request) {
    r.JSONP(w, http.StatusOK, "callbackName", map[string]string{"hello": "jsonp"})
})

Read the JSONP line closely. The callback name is an ordinary string argument, not something read off the request, so the endpoint it exposes is fixed when you write the handler.

XML defaults to an XHTML content type, HTML to text/html

Default content types are the first thing to check, because clients branch on them. JSON is application/json, JSONP is application/javascript, Text is text/plain, Data is application/octet-stream, HTML is text/html, and XML is application/xhtml+xml. Each of those is a field on render.Options, and Charset decides whether the charset is appended at all, since DisableCharset is false by default.

That XML entry is the odd one out, and it collides with the block above it. The available-options example offers `HTMLContentType: "application/xhtml+xml"` with a comment saying it outputs XHTML instead of the default text/html, and the defaults list then hands that identical string to XMLContentType while pinning HTMLContentType to text/html a few lines earlier. One media type is introduced as an opt-in override on the HTML route and shipped as the standing default on the XML route. A client that switches on content type to decide whether to run an XML parser receives an XHTML media type from the route most callers read as XML, and no paragraph in the documentation explains the choice.

Two further defaults matter when a response fails. PrefixJSON and PrefixXML are empty by default; the example fills them with a prefix built from the marker `)]}'` for JSON and the declaration `<?xml version='1.0' encoding='UTF-8'?>` for XML, described only as bytes prepended to the response. DisableHTTPErrorRendering is false by default, so an error raised during rendering turns into http.StatusInternalServerError instead of travelling back to your middleware.

Seven default keys never show up in the options example

Put the two Go blocks next to each other. The defaults block enumerates thirty keys on render.Options, and the available-options example sets twenty-three of them. Seven appear only in the defaults list: BinaryContentType, JSONContentType, JSONPContentType, TextContentType, XMLContentType, RenderPartialsWithoutPrefix and BufferPool.

Two of those omissions matter day to day. BinaryContentType is the only key that controls what r.Data announces, and it is exactly the one the example skips, so the passthrough route has a stated default with no stated way to change it. BufferPool defaults to GenericBufferPool while the repository layout keeps genericbufferpool.go next to sizedbufferpool.go at the top level, which makes swapping a field rather than a patch.

RenderPartialsWithoutPrefix gets neither prose nor an inline comment anywhere in the repository; the field name is the whole description. Coverage of the internals is thin in the same proportion: doc.go, engine.go, helpers.go and lock.go sit at the top level, none of them explained, and the README stops dead at a heading reading `Load` with no body beneath it, which is where template loading and reload behaviour would have been described.

No install line, only a module path and Go 1.25.0

The README prints no installation command, and nothing among the repository files substitutes one. What it does give is the module path github.com/unrolled/render and a go.mod whose directive reads go 1.25.0, so adding that import to your own module is the acquisition step and your toolchain has to sit at or above that version. Check it first, because a lower toolchain fails at build time and never reaches your handlers.

A first route needs one template file on disk. Templates load from the Directory value, which defaults to templates, and Extensions defaults to .tmpl, so the name you pass to r.HTML carries no extension. The shell command from the source comment creates the directory and the file in one step:

bash
$ mkdir -p templates && echo "<h1>Hello {{.}}.</h1>" > templates/example.tmpl

That markup is ordinary html/template syntax, where the dot stands for the value passed as the last argument. Wiring it to the mux looks exactly like the JSON routes:

go
mux.HandleFunc("/html", func(w http.ResponseWriter, req *http.Request) {
    r.HTML(w, http.StatusOK, "example", "World")
})

Start the process and open the /html path on the port from the example, and you get a heading with World substituted for the dot. No router library, no CLI and no background process are involved at any point.

IsDevelopment recompiles, UseMutexLock adds a lock

Template reloading is a flag here, not a watcher you write yourself. IsDevelopment is false by default and its documented effect, once set, is that templates are recompiled on every HTML response. go.mod pulls in github.com/fsnotify/fsnotify v1.10.0 for file system notifications, and the repository layout carries fs.go beside fs_embed.go with their own fs_test.go and fs_embed_test.go, so a plain directory and an embedded source both sit at the top level.

Left switched on in production, that flag moves template compilation onto the request path of every HTML route, which is why it ships false. Concurrency is a separate switch: UseMutexLock is also false by default and, when set, replaces that no lock implementation with the standard sync.RWMutex.

Lookup failures are quieter than that. RequirePartials returns an error when a template is missing a partial used in a layout, but only when the flag is true, and the default is false, so a layout naming a partial the directory does not hold renders without one and without raising anything. HTMLTemplateOption is empty by default and takes html/template option strings, with missingkey=error given as the example, which switches missing map keys from the package default to a template error.

StreamingJSON changes the write path, JSONP never streams

By default render.JSON marshals your object into a byte array first and writes that array to the http.ResponseWriter only once the marshal succeeds. Setting StreamingJSON to true switches to json.Encoder, which writes straight to the writer, and the documentation limits that to render.JSON: streaming is not implemented in render.JSONP, so a JSONP route keeps the buffered path even with the flag on.

The encoder itself is replaceable. JSONEncoder defaults to nil and takes a function returning a render.JSONEncoder, with jsoniter.NewEncoder(w) from "github.com/json-iterator" given as the worked example, the one place where response writing points outside the standard library. The same block offers IndentJSON and IndentXML for human readable output, both false by default, and UnEscapeHTML for getting `&<>` out correctly in JSON responses, also false by default.

go
StreamingJSON: true,   // Streams the JSON response via json.Encoder.
JSONEncoder: func(w io.Writer) render.JSONEncoder { // Use jsoniter "github.com/json-iterator"
    return jsoniter.NewEncoder(w)
},
IndentJSON: true,   // Output human readable JSON.
UnEscapeHTML: true, // Ensure '&<>' are output correctly (JSON only).

Look at those two JSON flags against each other before enabling both. One decides whether the response is buffered before its first byte goes out, the other decides what those bytes contain, and nothing in the documentation says which wins when IndentJSON is combined with StreamingJSON.

The standard library path costs three lines per handler

The honest alternative is to skip the package. encoding/json plus a Content-Type header plus w.Write is the whole of render.JSON, and an html/template parsed once at startup covers render.HTML with no dependency at all. That route costs nothing at build time and leaves every decision inside the handler where you can read it.

What Render buys is consolidation rather than new capability: one Options struct instead of per-handler headers, a template cache with Layout, Extensions, Delims and Funcs resolved once, the JSONP and prefix options, the error rendering path behind DisableHTTPErrorRendering, and the buffer pool hook. If your codebase already carries an internal package doing those things, adopting this one mostly means deleting yours.

The price is inheriting its rough edges. You take the application/xhtml+xml default on XML responses, the seven keys the examples never show, and documentation that ends mid-document at `Load`. For a service returning one JSON document from one handler, none of that earns its indirection.

go 1.25.0 and a single fsnotify dependency

The vendoring surface is small and traceable. go.mod requires github.com/fsnotify/fsnotify v1.10.0 and lists golang.org/x/sys v0.43.0 as indirect, so the only third-party code you take in is one file watching module. Everything else comes from the standard library, which keeps the upgrade bill short and keeps surprises inside your own handlers rather than in a tree of transitive packages.

Build checks are scripted, with one difference between the two targets. Running make test executes the linter, a race-enabled test run and the vetting; make ci runs the test and vet lines without golangci-lint:

bash
go test -cover -race -count=1 ./...
go vet ./...

The Makefile sets help as its default goal, so a bare make prints the target list rather than building anything. Terms are MIT, so no copyleft obligation travels with a vendored copy, and none of this is legal advice. Last push landed on 2026-09-14, and the v1.8.1 tag was published one second later the same day.

One naming detail survives a careful read of that tag list. The oldest of the three, v1.7.0, was published on 2024-09-29. The next one is spelled v.1.8.0 with a stray dot between the v and the digits, and it went out on 2026-05-01, while its neighbour v1.8.1 follows the pattern without the dot. Anything that matches tags against a strict pattern skips that entry, and a module pinned to the exact string has to carry the typo forward.

Editorial conclusion

Adopt unrolled/render when one service answers several content types from the same handler and you want a single Options struct to govern content types, template caching and JSON streaming. Skip it when a single JSON document is all you ever send, since encoding/json plus one header already covers that. Before wiring it in, check your toolchain against go 1.25.0, decide whether application/xhtml+xml on the XML route matches what your clients send, and confirm IsDevelopment is false in production.

Frequently asked questions

How do I install unrolled/render in a Go project?

The README prints no install command, and no other file in the repository supplies one. The identifiers it gives are the module path github.com/unrolled/render and the go 1.25.0 directive in go.mod, so the import you add to your own module is the whole acquisition step.

What is the difference between render.JSON and render.JSONP in unrolled/render?

render.JSON marshals through encoding/json and sends application/json. render.JSONP wraps the payload in the callback name you pass, shown as "callbackName" in the README example, and sends application/javascript by default. Streaming exists only in render.JSON, not in render.JSONP.

How do I make unrolled/render recompile templates during development?

Set IsDevelopment to true in render.Options; its documented effect is that templates are recompiled on every HTML response. The module depends on github.com/fsnotify/fsnotify v1.10.0, and UseMutexLock is the separate switch that replaces the default no lock implementation with sync.RWMutex.

What does unrolled/render do with binary responses?

r.Data passes the incoming data straight through to the http.ResponseWriter with no marshalling step, and BinaryContentType defaults to application/octet-stream. That key appears in the defaults list but not in the available-options example, so changing it has no worked example to copy.

What happens when a layout references a partial that does not exist in unrolled/render?

RequirePartials defaults to false. Its documented job is to return an error when a template is missing a partial used in a layout, so with the default in place the missing partial produces neither an error nor the content.

Official sources

  1. Issues
  2. License: MIT
  3. README
  4. Releases
  5. unrolled/render on GitHub
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/unrolled-render.svg)](https://hysenlabs.com/projects/unrolled-render)

Was this useful?