├── .gitignore ├── LICENSE ├── README.md ├── _example ├── custom_style.go ├── disable_color.go ├── format.go ├── oneline.go ├── pretty.go └── window.go ├── prettyjson.go └── prettyjson_test.go /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 2 | *.o 3 | *.a 4 | *.so 5 | 6 | # Folders 7 | _obj 8 | _test 9 | 10 | # Architecture specific extensions/prefixes 11 | *.[568vq] 12 | [568vq].out 13 | 14 | *.cgo1.go 15 | *.cgo2.c 16 | _cgo_defun.c 17 | _cgo_gotypes.go 18 | _cgo_export.* 19 | 20 | _testmain.go 21 | 22 | *.exe 23 | *.test 24 | *.prof 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Kazuhito Hokamura 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # prettyjson 2 | 3 | JSON pretty print for Golang. 4 | 5 | ## Example 6 | 7 | ```go 8 | import "github.com/hokaccha/go-prettyjson" 9 | 10 | v := map[string]interface{}{ 11 | "str": "foo", 12 | "num": 100, 13 | "bool": false, 14 | "null": nil, 15 | "array": []string{"foo", "bar", "baz"}, 16 | "map": map[string]interface{}{ 17 | "foo": "bar", 18 | }, 19 | } 20 | s, _ := prettyjson.Marshal(v) 21 | fmt.Println(string(s)) 22 | ``` 23 | 24 | ![Output](http://i.imgur.com/cUFj5os.png) 25 | 26 | ## License 27 | 28 | MIT 29 | -------------------------------------------------------------------------------- /_example/custom_style.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/fatih/color" 7 | "github.com/hokaccha/go-prettyjson" 8 | ) 9 | 10 | func main() { 11 | v := map[string]interface{}{ 12 | "str": "foo", 13 | "num": 100, 14 | "bool": false, 15 | "null": nil, 16 | "array": []string{"foo", "bar", "baz"}, 17 | "map": map[string]interface{}{ 18 | "foo": "bar", 19 | }, 20 | } 21 | f := prettyjson.NewFormatter() 22 | f.Indent = 4 23 | f.KeyColor = color.New(color.FgMagenta) 24 | f.BoolColor = nil 25 | f.NullColor = color.New(color.Underline) 26 | s, _ := f.Marshal(v) 27 | fmt.Println(string(s)) 28 | } 29 | -------------------------------------------------------------------------------- /_example/disable_color.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/hokaccha/go-prettyjson" 7 | ) 8 | 9 | func main() { 10 | v := map[string]interface{}{ 11 | "str": "foo", 12 | "num": 100, 13 | "bool": false, 14 | "null": nil, 15 | "array": []string{"foo", "bar", "baz"}, 16 | "map": map[string]interface{}{ 17 | "foo": "bar", 18 | }, 19 | } 20 | f := prettyjson.NewFormatter() 21 | f.DisabledColor = true 22 | s, _ := f.Marshal(v) 23 | fmt.Println(string(s)) 24 | } 25 | -------------------------------------------------------------------------------- /_example/format.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/hokaccha/go-prettyjson" 7 | ) 8 | 9 | func main() { 10 | s, _ := prettyjson.Format([]byte(`{"foo":"bar"}`)) 11 | fmt.Println(string(s)) 12 | } 13 | -------------------------------------------------------------------------------- /_example/oneline.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/hokaccha/go-prettyjson" 7 | ) 8 | 9 | func main() { 10 | v := map[string]interface{}{ 11 | "str": "foo", 12 | "num": 100, 13 | "bool": false, 14 | "null": nil, 15 | "array": []string{"foo", "bar", "baz"}, 16 | "map": map[string]interface{}{ 17 | "foo": "bar", 18 | }, 19 | } 20 | f := prettyjson.NewFormatter() 21 | f.Indent = 0 22 | f.Newline = "" 23 | s, _ := f.Marshal(v) 24 | fmt.Println(string(s)) 25 | } 26 | -------------------------------------------------------------------------------- /_example/pretty.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/hokaccha/go-prettyjson" 7 | ) 8 | 9 | func main() { 10 | v := map[string]interface{}{ 11 | "str": "foo", 12 | "num": 100, 13 | "bool": false, 14 | "null": nil, 15 | "array": []string{"foo", "", "baz"}, 16 | "map": map[string]interface{}{ 17 | "foo": "bar", 18 | }, 19 | } 20 | s, _ := prettyjson.Marshal(v) 21 | fmt.Println(string(s)) 22 | } 23 | -------------------------------------------------------------------------------- /_example/window.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/hokaccha/go-prettyjson" 7 | "github.com/mattn/go-colorable" 8 | ) 9 | 10 | func main() { 11 | v := map[string]interface{}{ 12 | "str": "foo", 13 | "num": 100, 14 | "bool": false, 15 | "null": nil, 16 | "array": []string{"foo", "bar", "baz"}, 17 | "map": map[string]interface{}{ 18 | "foo": "bar", 19 | }, 20 | } 21 | s, _ := prettyjson.Marshal(v) 22 | colorable.NewColorableStdout().Write(s) 23 | fmt.Print("\n") 24 | } 25 | -------------------------------------------------------------------------------- /prettyjson.go: -------------------------------------------------------------------------------- 1 | // Package prettyjson provides JSON pretty print. 2 | package prettyjson 3 | 4 | import ( 5 | "bytes" 6 | "encoding/json" 7 | "fmt" 8 | "sort" 9 | "strconv" 10 | "strings" 11 | 12 | "github.com/fatih/color" 13 | ) 14 | 15 | // Formatter is a struct to format JSON data. `color` is github.com/fatih/color: https://github.com/fatih/color 16 | type Formatter struct { 17 | // JSON key color. Default is `color.New(color.FgBlue, color.Bold)`. 18 | KeyColor *color.Color 19 | 20 | // JSON string value color. Default is `color.New(color.FgGreen, color.Bold)`. 21 | StringColor *color.Color 22 | 23 | // JSON boolean value color. Default is `color.New(color.FgYellow, color.Bold)`. 24 | BoolColor *color.Color 25 | 26 | // JSON number value color. Default is `color.New(color.FgCyan, color.Bold)`. 27 | NumberColor *color.Color 28 | 29 | // JSON null value color. Default is `color.New(color.FgBlack, color.Bold)`. 30 | NullColor *color.Color 31 | 32 | // Max length of JSON string value. When the value is 1 and over, string is truncated to length of the value. 33 | // Default is 0 (not truncated). 34 | StringMaxLength int 35 | 36 | // Boolean to disable color. Default is false. 37 | DisabledColor bool 38 | 39 | // Indent space number. Default is 2. 40 | Indent int 41 | 42 | // Newline string. To print without new lines set it to empty string. Default is \n. 43 | Newline string 44 | } 45 | 46 | // NewFormatter returns a new formatter with following default values. 47 | func NewFormatter() *Formatter { 48 | return &Formatter{ 49 | KeyColor: color.New(color.FgBlue, color.Bold), 50 | StringColor: color.New(color.FgGreen, color.Bold), 51 | BoolColor: color.New(color.FgYellow, color.Bold), 52 | NumberColor: color.New(color.FgCyan, color.Bold), 53 | NullColor: color.New(color.FgBlack, color.Bold), 54 | StringMaxLength: 0, 55 | DisabledColor: false, 56 | Indent: 2, 57 | Newline: "\n", 58 | } 59 | } 60 | 61 | // Marshal marshals and formats JSON data. 62 | func (f *Formatter) Marshal(v interface{}) ([]byte, error) { 63 | data, err := json.Marshal(v) 64 | if err != nil { 65 | return nil, err 66 | } 67 | 68 | return f.Format(data) 69 | } 70 | 71 | // Format formats JSON string. 72 | func (f *Formatter) Format(data []byte) ([]byte, error) { 73 | var v interface{} 74 | decoder := json.NewDecoder(bytes.NewReader(data)) 75 | decoder.UseNumber() 76 | if err := decoder.Decode(&v); err != nil { 77 | return nil, err 78 | } 79 | 80 | return []byte(f.pretty(v, 1)), nil 81 | } 82 | 83 | func (f *Formatter) sprintfColor(c *color.Color, format string, args ...interface{}) string { 84 | if f.DisabledColor || c == nil { 85 | return fmt.Sprintf(format, args...) 86 | } 87 | return c.SprintfFunc()(format, args...) 88 | } 89 | 90 | func (f *Formatter) sprintColor(c *color.Color, s string) string { 91 | if f.DisabledColor || c == nil { 92 | return fmt.Sprint(s) 93 | } 94 | return c.SprintFunc()(s) 95 | } 96 | 97 | func (f *Formatter) pretty(v interface{}, depth int) string { 98 | switch val := v.(type) { 99 | case string: 100 | return f.processString(val) 101 | case float64: 102 | return f.sprintColor(f.NumberColor, strconv.FormatFloat(val, 'f', -1, 64)) 103 | case json.Number: 104 | return f.sprintColor(f.NumberColor, string(val)) 105 | case bool: 106 | return f.sprintColor(f.BoolColor, strconv.FormatBool(val)) 107 | case nil: 108 | return f.sprintColor(f.NullColor, "null") 109 | case map[string]interface{}: 110 | return f.processMap(val, depth) 111 | case []interface{}: 112 | return f.processArray(val, depth) 113 | } 114 | 115 | return "" 116 | } 117 | 118 | func (f *Formatter) processString(s string) string { 119 | r := []rune(s) 120 | if f.StringMaxLength != 0 && len(r) >= f.StringMaxLength { 121 | s = string(r[0:f.StringMaxLength]) + "..." 122 | } 123 | 124 | buf := &bytes.Buffer{} 125 | encoder := json.NewEncoder(buf) 126 | encoder.SetEscapeHTML(false) 127 | encoder.Encode(s) 128 | s = string(buf.Bytes()) 129 | s = strings.TrimSuffix(s, "\n") 130 | 131 | return f.sprintColor(f.StringColor, s) 132 | } 133 | 134 | func (f *Formatter) processMap(m map[string]interface{}, depth int) string { 135 | if len(m) == 0 { 136 | return "{}" 137 | } 138 | 139 | currentIndent := f.generateIndent(depth - 1) 140 | nextIndent := f.generateIndent(depth) 141 | rows := []string{} 142 | keys := []string{} 143 | 144 | for key := range m { 145 | keys = append(keys, key) 146 | } 147 | 148 | sort.Strings(keys) 149 | 150 | for _, key := range keys { 151 | val := m[key] 152 | buf := &bytes.Buffer{} 153 | encoder := json.NewEncoder(buf) 154 | encoder.SetEscapeHTML(false) 155 | encoder.Encode(key) 156 | s := strings.TrimSuffix(string(buf.Bytes()), "\n") 157 | k := f.sprintColor(f.KeyColor, s) 158 | v := f.pretty(val, depth+1) 159 | 160 | valueIndent := " " 161 | if f.Newline == "" { 162 | valueIndent = "" 163 | } 164 | row := fmt.Sprintf("%s%s:%s%s", nextIndent, k, valueIndent, v) 165 | rows = append(rows, row) 166 | } 167 | 168 | return fmt.Sprintf("{%s%s%s%s}", f.Newline, strings.Join(rows, ","+f.Newline), f.Newline, currentIndent) 169 | } 170 | 171 | func (f *Formatter) processArray(a []interface{}, depth int) string { 172 | if len(a) == 0 { 173 | return "[]" 174 | } 175 | 176 | currentIndent := f.generateIndent(depth - 1) 177 | nextIndent := f.generateIndent(depth) 178 | rows := []string{} 179 | 180 | for _, val := range a { 181 | c := f.pretty(val, depth+1) 182 | row := nextIndent + c 183 | rows = append(rows, row) 184 | } 185 | return fmt.Sprintf("[%s%s%s%s]", f.Newline, strings.Join(rows, ","+f.Newline), f.Newline, currentIndent) 186 | } 187 | 188 | func (f *Formatter) generateIndent(depth int) string { 189 | return strings.Repeat(" ", f.Indent*depth) 190 | } 191 | 192 | // Marshal JSON data with default options. 193 | func Marshal(v interface{}) ([]byte, error) { 194 | return NewFormatter().Marshal(v) 195 | } 196 | 197 | // Format JSON string with default options. 198 | func Format(data []byte) ([]byte, error) { 199 | return NewFormatter().Format(data) 200 | } 201 | -------------------------------------------------------------------------------- /prettyjson_test.go: -------------------------------------------------------------------------------- 1 | package prettyjson 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "strings" 7 | "testing" 8 | 9 | "github.com/fatih/color" 10 | ) 11 | 12 | func Example() { 13 | defer func(noColor bool) { color.NoColor = noColor }(color.NoColor) 14 | color.NoColor = false 15 | 16 | v := map[string]interface{}{ 17 | "str": "foo", 18 | "num": 100, 19 | "bool": false, 20 | "null": nil, 21 | "array": []string{"foo", "bar", "baz"}, 22 | "map": map[string]interface{}{ 23 | "foo": "bar", 24 | }, 25 | } 26 | s, _ := Marshal(v) 27 | fmt.Println(string(s)) 28 | // Output: 29 | // { 30 | // "array": [ 31 | // "foo", 32 | // "bar", 33 | // "baz" 34 | // ], 35 | // "bool": false, 36 | // "map": { 37 | // "foo": "bar" 38 | // }, 39 | // "null": null, 40 | // "num": 100, 41 | // "str": "foo" 42 | // } 43 | } 44 | 45 | func TestMarshal(t *testing.T) { 46 | prettyJSON := func(s string) string { 47 | var v interface{} 48 | 49 | decoder := json.NewDecoder(strings.NewReader(s)) 50 | decoder.UseNumber() 51 | err := decoder.Decode(&v) 52 | 53 | if err != nil { 54 | t.Error(err) 55 | } 56 | 57 | prettyJSONByte, err := Marshal(v) 58 | 59 | if err != nil { 60 | t.Error(err) 61 | } 62 | 63 | return string(prettyJSONByte) 64 | } 65 | 66 | test := func(expected, actual string) { 67 | if expected != actual { 68 | t.Errorf("\nexpected:\n%s\n\nactual:\n%s", expected, actual) 69 | } 70 | } 71 | 72 | blueBold := color.New(color.FgBlue, color.Bold).SprintFunc() 73 | greenBold := color.New(color.FgGreen, color.Bold).SprintFunc() 74 | cyanBold := color.New(color.FgCyan, color.Bold).SprintFunc() 75 | blackBold := color.New(color.FgBlack, color.Bold).SprintFunc() 76 | yelloBold := color.New(color.FgYellow, color.Bold).SprintFunc() 77 | 78 | actual := prettyJSON(`{ 79 | "key": { 80 | "a": "str", 81 | "b": 100, 82 | "c": null, 83 | "d": true, 84 | "e": false, 85 | "f": { "key": "str" }, 86 | "g": {}, 87 | "h": [] 88 | } 89 | }`) 90 | 91 | expectedFormat := `{ 92 | %s: { 93 | %s: %s, 94 | %s: %s, 95 | %s: %s, 96 | %s: %s, 97 | %s: %s, 98 | %s: { 99 | %s: %s 100 | }, 101 | %s: {}, 102 | %s: [] 103 | } 104 | }` 105 | 106 | expected := fmt.Sprintf(expectedFormat, 107 | blueBold(`"key"`), 108 | blueBold(`"a"`), greenBold(`"str"`), 109 | blueBold(`"b"`), cyanBold("100"), 110 | blueBold(`"c"`), blackBold("null"), 111 | blueBold(`"d"`), yelloBold("true"), 112 | blueBold(`"e"`), yelloBold("false"), 113 | blueBold(`"f"`), blueBold(`"key"`), greenBold(`"str"`), 114 | blueBold(`"g"`), 115 | blueBold(`"h"`), 116 | ) 117 | 118 | test(expected, actual) 119 | test("{}", prettyJSON("{}")) 120 | test("[]", prettyJSON("[]")) 121 | 122 | test( 123 | fmt.Sprintf("{\n %s: %s\n}", blueBold(`"x"`), cyanBold("123456789123456789123456789")), 124 | prettyJSON(`{"x":123456789123456789123456789}`), 125 | ) 126 | 127 | test( 128 | fmt.Sprintf("{\n %s: %s\n}", blueBold(`"foo\"bar\n\r\t<>★"`), cyanBold("1")), 129 | prettyJSON(`{"foo\"bar\n\r\t<>★":1}`), 130 | ) 131 | } 132 | 133 | func TestStringEscape(t *testing.T) { 134 | f := NewFormatter() 135 | f.DisabledColor = true 136 | s := `{"foo":"foo\"\nbar"}` 137 | r, err := f.Format([]byte(s)) 138 | 139 | if err != nil { 140 | t.Error(err) 141 | } 142 | 143 | expected := `{ 144 | "foo": "foo\"\nbar" 145 | }` 146 | 147 | if string(r) != expected { 148 | t.Errorf("actual: %s\nexpected: %s", string(r), expected) 149 | } 150 | } 151 | 152 | func TestStringPercentEscape(t *testing.T) { 153 | f := NewFormatter() 154 | s := `{"foo":"foo%2Fbar"}` 155 | r, err := f.Format([]byte(s)) 156 | 157 | if err != nil { 158 | t.Error(err) 159 | } 160 | 161 | expectedFormat := `{ 162 | %s: %s 163 | }` 164 | 165 | blueBold := color.New(color.FgBlue, color.Bold).SprintFunc() 166 | greenBold := color.New(color.FgGreen, color.Bold).SprintFunc() 167 | 168 | expected := fmt.Sprintf(expectedFormat, 169 | blueBold(`"foo"`), greenBold(`"foo%2Fbar"`), 170 | ) 171 | 172 | if string(r) != expected { 173 | t.Errorf("actual: %s\nexpected: %s", string(r), expected) 174 | } 175 | } 176 | 177 | func TestStringPercentEscape_DisabledColor(t *testing.T) { 178 | f := NewFormatter() 179 | f.DisabledColor = true 180 | s := `{"foo":"foo%2Fbar"}` 181 | r, err := f.Format([]byte(s)) 182 | 183 | if err != nil { 184 | t.Error(err) 185 | } 186 | 187 | expected := `{ 188 | "foo": "foo%2Fbar" 189 | }` 190 | 191 | if string(r) != expected { 192 | t.Errorf("actual: %s\nexpected: %s", string(r), expected) 193 | } 194 | } 195 | 196 | func BenchmarkFromat(b *testing.B) { 197 | s := []byte(`{"string": "a", "number": 3, "array": [1, 2, 3], "map": {"map": "value"}, "emptyArray": [], "emptyMap": {}}`) 198 | f := NewFormatter() 199 | 200 | if _, err := f.Format(s); err != nil { 201 | b.Fatal(err) 202 | } 203 | for i := 0; i < b.N; i++ { 204 | f.Format(s) 205 | } 206 | } 207 | --------------------------------------------------------------------------------