Why I love gjson

Thu, Jan 4, 2024 2-minute read

Why I love gjson

Easy parsing. That’s it. That’s the reason.

I have these structs

type ApiResponse struct {
	Message string `json:"message"`
	Status  string `json:"status"`
	Code    int    `json:"code"`
	Return  int    `json:"return"`
}
type SystemDNSResponse struct {
	Data *pfsense.DNS `json:"data"`
	ApiResponse
}

When I make a request to the client I’ll get a response that looks something like this:

{
  "status": "ok",
  "code": 200,
  "return": 0,
  "message": "Success",
  "data": {
    "dnsserver": [
      "1.1.1.1",
      "8.8.8.8"
    ],
    "dnsallowoverride": false,
    "dnslocalhost": true
  }
}

I can marshall this into that struct easily using the built in encoding/json package.

But, I want a generic methods which can be passed in a struct and do the unmarshalling further down the line.

This is where gjson really shines; it can inspect []byte and output the string which I can then do something with. I don’t need any struct just the field name I want.

For example, I care about the code field. And have functions which will return errors based on its value:

// checkRawJsonStatusCode ensures that any non-200 status codes return an error
// from the firewall. The response from the firewall is not a http/net object
// so, we must manually inspect the code int and derive their meaning here.
func (s *pfsensesrvc) checkRawJsonStatusCode(code int) error {
	switch {
	case code >= 200 && code <= 299:
		break
	default:
		return fmt.Errorf("failed client validation of pfsense api: %d", code)
	}
	return nil
}

This function takes a string which would require unmarshalling to extract.

With gjson I can pass in the []byte and grab it out easily, like this:

// apiResponseCode returns the pfSense-api 'code' value from a successful API
// call to the client API.
func apiResponseCode(jsn []byte) int {
	const key = "code"
	c := gjson.GetBytes(jsn, key)
	return int(c.Num)
}

// MarshallAPIResponse parses weakly typed responses from the client device and marshall's
// it into a struct.
func (s *pfsensesrvc) MarshallAPIResponse(b []byte, result any) error {
  // truncated the rest of this method
	err = s.checkRawJsonStatusCode(apiResponseCode(b))
	if err != nil {
		return err
	}
	return nil
}

You can see that I grab the code field as an int which is passed in as an argument to checkRawJsonStatusCode.

I do it this way because in MarshallAPIResponse I only have to pass in []byte whereas if I used encoding/json I would need to be more explicit about the type.

Personally, I’ve found it to be invaluable, especially when creating an API which has many tens of endpoints, each returning their own types.

Tags:

#json #go #gjson