luna

package module
v0.3.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Sep 2, 2022 License: MIT Imports: 4 Imported by: 1

README

Better Parsing of Unstructured JSON in Go


Goal

The intent of luna is to create a simpler interface to work with when parsing JSON in Go without having to create a bunch of structs with tags.

Features include:

  • An interface that propagates errors until you finally extract a value to use
  • More detailed error messages that give valuable debugging information (such as other keys in the map when a key is not found)
  • Equivalent versions of value-retrieving functions to panic on error (but allow nesting results inside function calls)

Motivation

Parsing unstructured JSON is plain ugly in Go. Let's say you have the below JSON and you want to pull out the first person's score:

data := []byte(`{
    "people": [
        {
            "name": "alice",
            "score": 89.5,
            "friends": [ "bob" ],
            "deleted": false
        },
        {
            "name": "bob",
            "score": 75.5,
            "friends": [],
            "deleted": false,
        }
    ]
}`)

If you didn't want to go through the hassle of creating structs that represent each of the entities in the JSON, you would have to do something like this:

var dataMap map[string]interface{}
err := json.Unmarshal(data, &dataMap)
if err != nil {
	return err
}
peopleObj, ok := dataMap["people"] // peopleObj is an `interface{}`
if !ok {
	return fmt.Errorf("people key not found")
}
peopleArray, ok := peopleObj.([]interface{})
if !ok {
	return fmt.Errorf("people should be a []interface{}, but is a %T", peopleArray)
}
if len(peopleArray) == 0 {
	return fmt.Errorf("no people found")
}
firstPerson := peopleObj[0] // firstPerson is an `interface{}`
firstPersonMap, ok := firstPerson.(map[string]interface{})
if !ok {
	return fmt.Errorf("first person should be a map[string]interface{}, but is a %T", firstPerson)
}
firstPersonScore, ok := firstPersonMap["score"] // firstPersonScore is an `interface{}`
if !ok {
	return fmt.Errorf("score not found")
}
score, ok := firstPersonScore.(float64)
if !ok {
	return fmt.Errorf("score should be a float64, but is a %T", score)
}

Phew! That was a lot of code to get one little value out of a tiny JSON. Is it possible to write this more concisely but retain the all the validation that guards us against the data not being in the expected format?

Simple Code Without Sacrifice

Using this library, here's how you would pull out the same value and still include all the useful validation done above...

score, err := luna.MapFromBytes(data).Array("people").Map(0).Float("score")
if err != nil {
    return err
}
fmt.Printf("Alice's score: %f\n", score)
Alice's score: 89.5

But what if we got the key wrong and used "grade" instead of "score"?

score, err := luna.MapFromBytes(data).Array("people").Map(0).Float("grade")
if err != nil {
    fmt.Printf("Uh oh! %s\n", err)
}
Uh oh! key 'grade' not found at path $['people'][0], valid keys: name, score, friends, deleted

Well that's much cleaner! And we didn't have to do annoying type casting from interface{} or error checking along the way. Just check once when you pull out the value and if any of the errors that we checked for in the old code happened, they will be propagated to the final call to get the values out of the JSON. As a bonus, the error message conveniently contains the full path to where the error happened along with the valid keys at that path!

As another example, showing the power of the error propagation, what if we got the people key wrong and used folks?

score, err := luna.MapFromBytes(data).Array("folks").Map(0).Float("grade")
if err != nil {
    fmt.Printf("Uh oh! %s\n", err)
}
Uh oh! key 'folks' not found at path $, valid keys: people

Even though the error occurred earlier on in the chained calls, we still see the original error!

Wow! How did that work?

Actually, it's not that complicated. Whenever you call .Array(...) or .Map(...) with an invalid index or key, it returns a struct that holds on to that error and finally returns it when you call one of the other functions like Float(...). Meanwhile, it also builds up and keeps track of the path so that if any error does happen, you'll know exactly where to look!

So what's the full interface?

Check it out...

Documentation

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

This section is empty.

Types

type Array

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

Array provides methods to either navigate through the content of a JSON array or propagate any error that has occurred

func ArrayFromBytes

func ArrayFromBytes(jsonBytes []byte) Array

ArrayFromBytes creates an Array from a []byte

func ArrayFromReader

func ArrayFromReader(r io.Reader) Array

ArrayFromReader creates an Array from an io.Reader

func NewArray

func NewArray(a []interface{}) Array

NewArray creates an Array from a []interface{}

func (Array) Array

func (a Array) Array(idx int) Array

Array returns the array found at index `idx` in the array; errors will be propagated

func (Array) Bool

func (a Array) Bool(idx int) (bool, error)

Bool returns the value of a bool at index `idx` in the array, or a propagated error

func (Array) Bytes

func (a Array) Bytes() ([]byte, error)

Bytes returns the serialized value into a slice of bytes, or a propagated error

func (Array) Err

func (a Array) Err() error

Err returns any error that was found up to this point

func (Array) Float

func (a Array) Float(idx int) (float64, error)

Float returns the value of a float at index `idx` in the array, or a propagated error

func (Array) Inner added in v0.3.0

func (a Array) Inner() ([]interface{}, error)

Inner returns the `[]interface{}` which this `Array` represents, or a propagated error

func (Array) Len

func (a Array) Len() (int, error)

Len returns the length of the array, or a propagated error

func (Array) Map

func (a Array) Map(idx int) Map

Map returns the map found at index `idx` in the array; errors will be propagated

func (Array) String

func (a Array) String(idx int) (string, error)

String returns the value of a string at index `idx` in the array, or a propagated error

type Map

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

Map provides methods to either navigate through the content of a JSON object or propagate any error that has occurred

func MapFromBytes

func MapFromBytes(jsonBytes []byte) Map

MapFromBytes creates a Map from a []byte

func MapFromReader

func MapFromReader(r io.Reader) Map

MapFromReader creates a Map from an io.Reader

func NewMap

func NewMap(m map[string]interface{}) Map

NewMap creates a Map from a map[string]interface{}

func (Map) Array

func (m Map) Array(key string) Array

Array returns the array found at key `key` in the map; errors will be propagated

func (Map) Bool

func (m Map) Bool(key string) (bool, error)

Bool returns the value of a bool at key `key` in the map, or a propagated error

func (Map) Bytes

func (m Map) Bytes() ([]byte, error)

Bytes returns the serialized value into a slice of bytes, or a propagated error

func (Map) Err

func (m Map) Err() error

Err returns any error that was found up to this point

func (Map) Float

func (m Map) Float(key string) (float64, error)

Float returns the value of a float at key `key` in the map, or a propagated error

func (Map) Has

func (m Map) Has(key string) (bool, error)

Has returns true if the map contains the key `key`, or a propagated error

func (Map) Inner

func (m Map) Inner() (map[string]interface{}, error)

Inner returns the `map[string]interface{}` which this `Map` represents, or a propagated error

func (Map) Map

func (m Map) Map(key string) Map

Map returns the map found at key `key` in the map; errors will be propagated

func (Map) String

func (m Map) String(key string) (string, error)

String returns the value of a string at key `key` in the map, or a propagated error

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL