160

Question: Currently I'm printing out my response in the func Index like this fmt.Fprintf(w, string(response)) however, how can I send JSON properly in the request so that it maybe consumed by a view?

package main

import (
    "fmt"
    "github.com/julienschmidt/httprouter"
    "net/http"
    "log"
    "encoding/json"
)

type Payload struct {
    Stuff Data
}
type Data struct {
    Fruit Fruits
    Veggies Vegetables
}
type Fruits map[string]int
type Vegetables map[string]int


func Index(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
    response, err := getJsonResponse();
    if err != nil {
        panic(err)
    }
    fmt.Fprintf(w, string(response))
}


func main() {
    router := httprouter.New()
    router.GET("/", Index)
    log.Fatal(http.ListenAndServe(":8080", router))
}

func getJsonResponse()([]byte, error) {
    fruits := make(map[string]int)
    fruits["Apples"] = 25
    fruits["Oranges"] = 10

    vegetables := make(map[string]int)
    vegetables["Carrats"] = 10
    vegetables["Beets"] = 0

    d := Data{fruits, vegetables}
    p := Payload{d}

    return json.MarshalIndent(p, "", "  ")
}
John Topley
  • 113,588
  • 46
  • 195
  • 237
Armeen Moon
  • 18,061
  • 35
  • 120
  • 233

6 Answers6

212

You can set your content-type header so clients know to expect json

w.Header().Set("Content-Type", "application/json")

Another way to marshal a struct to json is to build an encoder using the http.ResponseWriter

// get a payload p := Payload{d}
json.NewEncoder(w).Encode(p)
dm03514
  • 54,664
  • 18
  • 108
  • 145
  • 22
    While `w.Header().Set("Content-Type", "application/json")` is correct for setting the content type, it doesn't when using `json.NewEncoder` instead i get a txt/plain result. Is anyone else getting this. The answer from @poorva worked as expected – Jaybeecave Apr 30 '17 at 00:48
  • 3
    Scratch that. If i use `w.WriteHeader(http.StatusOk) ` I get the above result. – Jaybeecave Apr 30 '17 at 00:50
  • 4
    If I use `w.WriteHeader(http.StatusOk)` then I get `text/plain; charset=utf-8`, if I dont set the Status-Code explicitly I get `applicaton/json` and the Response has still a Status-Code 200. – Ramon Rambo Sep 15 '17 at 21:18
  • 6
    Hmmm ... could it have to do with the docs [here](https://golang.org/pkg/net/http/#ResponseWriter)? `Changing the header map after a call to WriteHeader (or Write) has no effect unless the modified headers are trailers.` – Dan Esparza Nov 18 '17 at 19:43
  • 5
    Adding `w.Header().Set("Content-Type", "application/json")` above `json.NewEncoder(w).Encode(p)` work for me – Ardi Nusawan Mar 08 '18 at 09:30
  • @RamonRambo adding w.Header().Set("Content-Type", "application/json") above w.WriteHeader(http.StatusOk) worked for me. – Gokul Raj Kumar Aug 30 '18 at 14:50
  • 1
    The problem with this answer is... What if the Encode fails? Sadly, it can happen, for example if a float64 value is NaN! It will return a 200 without body, and you'll find yourself yelling "WHAT THE HELL" to your code. We should check the response value, but in that case, we could not change the Header no more... a dog chasing its own tail... If you care about your sanity, please use @poorva response. – Cirelli94 Dec 02 '20 at 09:53
  • Another subtle difference is that the encoder will add a newline at the end of the HTTP Body as part of the payload (content length). This does not happen if json.Marshal() & w.write() is used as outlined in the answer post by @poorva – Marcus Apr 21 '22 at 09:02
71

Other users were commenting that the Content-Type is plain/text when encoding.
You have to set the content type with w.Header().Set() first, then write the HTTP response code with w.WriteHeader().

If you call w.WriteHeader() first, then call w.Header().Set() after you will get plain/text.

An example handler might look like this:

func SomeHandler(w http.ResponseWriter, r *http.Request) {
    data := SomeStruct{}
    w.Header().Set("Content-Type", "application/json")
    w.WriteHeader(http.StatusCreated)
    json.NewEncoder(w).Encode(data)
}
U. Windl
  • 3,480
  • 26
  • 54
Daniel R.
  • 811
  • 6
  • 5
51

You can do something like this in you getJsonResponse function -

jData, err := json.Marshal(Data)
if err != nil {
    // handle error
}
w.Header().Set("Content-Type", "application/json")
w.Write(jData)
CommonSenseCode
  • 23,522
  • 33
  • 131
  • 186
poorva
  • 1,726
  • 1
  • 17
  • 15
  • 3
    One important note about this version is that it uses a byte slice in `jData`, possibly unnecessarily. `Data` can be of arbitrary size, depending on the data being marshalled, so this could be a non-trivial memory waster. After marshalling, we copy from memory to the `ResponseWriter` stream. The answer that uses json.NewEncoder() etc. would write the marshalled JSON straight into the `ResponseWriter` (into its stream ..) – Jonno Feb 28 '17 at 14:19
  • 1
    Worked for me! Faced the issue when 'w.WriteHeader(http.StatusCreated)' was added before or after. – darkdefender27 Sep 25 '17 at 14:25
  • 1
    No need to return after panic as this exits your program – andersfylling Oct 31 '17 at 23:50
  • At least this solution doesn't add the trailing \n of the `Encoder.Encode()` function – Jonathan Muller Apr 29 '19 at 01:47
  • 1
    @Jonno you are right, but it's the only answer when you can check the encode goes well BEFORE writing the header, because once written it can be changed! – Cirelli94 Dec 02 '20 at 09:45
  • This is the only safe answer! – Cirelli94 Dec 02 '20 at 09:47
3

In gobuffalo.io framework I got it to work like this:

// say we are in some resource Show action
// some code is omitted
user := &models.User{}
if c.Request().Header.Get("Content-type") == "application/json" {
    return c.Render(200, r.JSON(user))
} else {
    // Make user available inside the html template
    c.Set("user", user)
    return c.Render(200, r.HTML("users/show.html"))
}

and then when I want to get JSON response for that resource I have to set "Content-type" to "application/json" and it works.

I think Rails has more convenient way to handle multiple response types, I didn't see the same in gobuffalo so far.

Aleks Tkachenko
  • 704
  • 7
  • 6
1

You may use this package renderer, I have written to solve this kind of problem, it's a wrapper to serve JSON, JSONP, XML, HTML etc.

1

This is a complement answer with a proper example:

func (ch captureHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    switch r.Method {
    case http.MethodPost:
        body, err := ioutil.ReadAll(r.Body)
        if err != nil {
            http.Error(w, fmt.Sprintf("error reading request body, %v", err), http.StatusInternalServerError)
            return
        }
        ...do your stuff here...
    case http.MethodGet:
        w.Header().Set("Content-Type", "application/json")
        err := json.NewEncoder(w).Encode( ...put your object here...)
        if err != nil {
            http.Error(w, fmt.Sprintf("error building the response, %v", err), http.StatusInternalServerError)
            return
        }
    default:
        http.Error(w, fmt.Sprintf("method %s is not allowed", r.Method), http.StatusMethodNotAllowed)
    }
}
Mircea Stanciu
  • 3,675
  • 3
  • 34
  • 37