README

Welcome to dcode 🎉

Build Status Go Report Card GoDoc

Note: this library is a work in progress. I'd love for you to try it! If you do find issues, please file an issue. Happy hacking!

Welcome Gophers! dcode (pronounced "decode") is a library that gives you a quick and easy way to quickly traverse JSON objects, target a single field, and pull it out into a variable you specify. All using a familiar API.

dcode tries to help you with these two cases in your everyday programming life:

  1. When you don't know the "shape" of the JSON coming into your program
  2. When you know there's a huge JSON object coming in, but you only need to get a few values out of it

To accomplish the first case without dcode, you'd usually decode the JSON into a map[string]interface{}, and then deal with the untyped interface{} values as you traverse your way down into the map to get the value you want.

To accomplish the second case without dcode, you'd usually write a bunch of structs so you can decode the values you want (one per "level" in the JSON).

Ok, Tell Me More!

We're going to focus on the "let's decode big JSON objects" use case in this section.

Let's say you're a weather buff, and you want to write some Go to get the forecast from Dark Sky using their API. Awesome!

Here's part of a JSON response from Dark Sky's API (adapted from their docs page):

{
    "latitude": 42.3601,
    "longitude": -71.0589,
    "timezone": "America/New_York",
    "minutely": {
        "summary": "Light rain stopping in 13 min., starting again 30 min. later.",
        "icon": "rain",
        "data": {
            "time": 1509993240,
            "precipIntensity": 0.007,
            "precipIntensityError": 0.004,
            "precipProbability": 0.84,
            "precipType": "rain"
        }
    }
}

Let's try to get the precipitation probability and type so we can print out "84% chance of rain."

Let's Decode This With encoding/json

The most common way you decode JSON using encoding/json is to decode a []byte into a struct. Here's what the struct for this JSON response would look like:

type minutelyResp struct {
    precipType string `json:"precipType"`
    precipProbability float64 `json:"precipProbability"`
}

type forecast struct {
    minutely minutelyResp `json:"minutely"`
}

Then, here's how we'd decode the struct:

fcast := new(forecast)
// TODO: deal with the error!
json.Unmarshal(jsonBytes, fcast)

Not terrible, but it's a little boilerplate-ey just to get a string and a float64 out of this response. If there were values nested even deeper, we'd have to write more structs to get them.

Note: If you have to start grabbing a ton more data from the API response JSON, it might make sense to fill out the struct more and grab the data you need.

Let's Decode The Same JSON With dcode

We can decode the same JSON with a little bit less boilerplate, and be clearer about the JSON we're trying to get!

To start, here's how we'd use dcode to do the same decoding:

typeDecoder := First("minutely").Then("precipType").Into(String())
probDecoder := First("minutely").Then("precipProbability").Into(Float64())
var precipType string
var prob float64
// TODO: deal with the errors here!
Decode(typeDecoder, jsonBytes, &precipType) 
Decode(probDecoder, jsonBytes, &prob)

It looks a fair bit different than the previous example with encoding/json. How is this different? Why is it better? Here are some answers for you:

  • No boilerplate structs to write. Hooray!
  • You target just the fields you want. No more mistakes with struct tags 🔥
  • You write how to traverse the returned JSON rather than the complete structure you expect. Don't fail because some other field came in slightly differently than you expeced
  • Your parsing code tends to be more self-documenting. It almost looks like JSONPath!
  • You can reuse those *Decoder values as many times as you want, against any []byte, without creating any new structs or allocating any new memory
Interesting, Right?

Check out USAGE.md for more details, and enjoy!

Notes

This library follows patterns from the Elm language JSON decoding library.

I love that language, and I think their JSON decoder is really well designed. The described the benefits to their decoder pattern in their documenation, and I slighly adapted what they wrote here 😉:

Fun Fact: I have heard a bunch of stories of folks finding bugs in their server code as they switched from JS encoding/json to Elm dcode. The decoders people write end up working as a validation phase, catching weird stuff in JSON values. So when NoRedInk you switched from React encoding/json to Elm dcode, it revealed a couple bugs in their Ruby server code!

Documentation

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func Decode

func Decode(b []byte, d Decoder, i interface{}) error

    Decode decodes b into i using d

    func Exists

    func Exists(decoder Decoder, bytes []byte) bool

      Exists checks whether the value that decoder points to (in the JSON bytes) exists and is the expected type

      func Map

      func Map(
      	b []byte,
      	pairs ...MapPair,
      ) error

        Map decodes separate JSON fields into separate values, using separate decoders, all from the same JSON.

        For example, if you have the following JSON:

        json := `{"field1": 123, "field2": "456"}`
        

        And you have the following decoders:

        dcode1 := Field("field1", Int())
        dcode2 := Field("field2", String())
        

        You can do the following:

        stucco := struct{
        	field1 int
        	field2 string
        }
        
        // check the error here!
        Map(
        	[]byte(json),
        	Pair(dcode1, &stucco.field1),
        	Pair(dcode2, &stucco.field2),
        )
        

        Note: you can decode JSON into structs using this function, but if you're trying to decode tons of fields from JSON to struct fields, you might be better off using encoding/json!

        func OneOf

        func OneOf(b []byte, dList []Decoder, i interface{}) error

          OneOf tries to decode b into i using every decoder in dList.

          It returns nil after the first decoder succeeds. If none succeeded, returns an error

          Types

          type Builder

          type Builder struct {
          	// contains filtered or unexported fields
          }

            Builder is a convenience struct that lets you write decoders as a chain, rather than in the "pure functional" style. Instead of this:

            dcoder := Field("first", Field("second", Field("third", Int())))
            

            You can write this:

            dcoder := First("first").Then("second").Then("third").Into(Int())
            

            func First

            func First(field string) Builder

              First returns a Builder that starts at field in the JSON object

              func (Builder) Into

              func (b Builder) Into(d Decoder) Decoder

                Into returns a Decoder that decodes the value at the current travere level using d. See the documentation in Builder for a complete explanation

                func (Builder) Then

                func (b Builder) Then(field string) Builder

                  Then traverses one level down into the JSON object. See the documentation in Builder for a complete explanation

                  type Decoder

                  type Decoder struct {
                  	// contains filtered or unexported fields
                  }

                    Decoder is the core type in dcode. This is basically a pure function that can decode ANY JSON value into a particular type.

                    You can't implement one of these directly. Instead use one of the built-in ones like Int(), String(), etc... and build them up with Field(...) or First(...).Then(...).Into(...).

                    Check out the documentation for Field() or Builder for more information

                    func Bool

                    func Bool() Decoder

                      Bool decodes any JSON field into a bool

                      func Field

                      func Field(name string, decoder Decoder) Decoder

                        Field builds up a traversal. Here's an example that starts with "field1", then goes down to "field2", and then into "field3", and decodes that value into an int:

                        dcoder := Field("field1", Field("field2", Field("field3", Int())))
                        

                        If you have the following JSON:

                        {
                        	"field1": {
                        		"field2": {
                        			"field3": 123
                        		}
                        	}
                        }
                        

                        Then the following code would decode the integer 123 into the variable i

                        var i int
                        Decode(dcoder, jsonBytes, &i) // make sure to check the error returned!
                        

                        func Float64

                        func Float64() Decoder

                          Float64 decodes any JSON field into a float64

                          func Int

                          func Int() Decoder

                            Int decodes any JSON field into an integer

                            func String

                            func String() Decoder

                              String decodes any JSON field into a string

                              type ErrWrongType

                              type ErrWrongType struct {
                              	// contains filtered or unexported fields
                              }

                              func (ErrWrongType) Error

                              func (e ErrWrongType) Error() string

                              type MapPair

                              type MapPair struct {
                              	// contains filtered or unexported fields
                              }

                                MapPair is a value to pass into the Map function. Create one of these with the Pair function, and see the documentation under the Map function for where this is used

                                func Pair

                                func Pair(d Decoder, iface interface{}) MapPair

                                  Pair returns a new MapPair