diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
new file mode 100644
index 0000000..07177f9
--- /dev/null
+++ b/.github/workflows/test.yml
@@ -0,0 +1,34 @@
+name: Run tests with coverage 
+
+on:
+  push:
+
+jobs:
+  test:
+    runs-on: ubuntu-20.04
+
+    steps:
+    - name: Check out code
+      uses: actions/checkout@v3
+
+    - name: Set up Go
+      uses: actions/setup-go@v3
+      with:
+        go-version: '1.18'
+
+    - name: Lint code 
+      uses: golangci/golangci-lint-action@v3
+      with:
+        version: v1.45.2
+        args: --timeout 2m0s
+
+    - name: Vet code
+      run: go vet ./...
+
+    - name: Run test
+      run: ./test.sh
+    
+    - name: Upload coverage to Codecov
+      uses: codecov/codecov-action@v2
+      with:
+        files: ./coverage.txt
\ No newline at end of file
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..80231fc
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,19 @@
+Copyright (c) 2022  Adrien Petel
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
\ No newline at end of file
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..76a730c
--- /dev/null
+++ b/README.md
@@ -0,0 +1,41 @@
+[![Go Report Card](https://goreportcard.com/badge/github.com/feliixx/boa)](https://goreportcard.com/report/github.com/feliixx/boa)
+[![codecov](https://codecov.io/gh/feliixx/boa/branch/master/graph/badge.svg)](https://codecov.io/gh/feliixx/boa)
+[![PkgGoDev](https://pkg.go.dev/badge/github.com/feliixx/boa)](https://pkg.go.dev/github.com/feliixx/boa)
+
+## boa
+
+A small configuration library for go application with a viper-like API, but with limited scope and no external dependency 
+
+It supports: 
+
+  * reading a config in JSON format 
+  * setting default 
+
+
+
+## example
+
+
+```go
+config := `
+{
+  "http_server": {
+    "enabled": true,
+    "host": "127.0.0.1"
+  }
+}`
+
+boa.SetDefault("http_server.port", 80)
+
+err := boa.ParseConfig(strings.NewReader(config))
+if err != nil {
+	log.Fatal(err)
+}
+
+srvHost := boa.GetString("http_server.host")
+srvPort := boa.GetInt("http_server.port")
+
+fmt.Printf("%s:%d", srvHost, srvPort)
+// Output: 127.0.0.1:80
+```
+
diff --git a/example_test.go b/example_test.go
new file mode 100644
index 0000000..ab776ba
--- /dev/null
+++ b/example_test.go
@@ -0,0 +1,33 @@
+package boa_test
+
+import (
+	"fmt"
+	"log"
+	"strings"
+
+	"github.com/feliixx/boa"
+)
+
+func Example() {
+
+	config := `
+	{
+		"http_server": {
+			"enabled": true,
+			"host": "127.0.0.1"
+		}
+	}`
+
+	boa.SetDefault("http_server.port", 80)
+
+	err := boa.ParseConfig(strings.NewReader(config))
+	if err != nil {
+		log.Fatal(err)
+	}
+
+	srvHost := boa.GetString("http_server.host")
+	srvPort := boa.GetInt("http_server.port")
+
+	fmt.Printf("%s:%d", srvHost, srvPort)
+	// Output: 127.0.0.1:80
+}
diff --git a/go.mod b/go.mod
new file mode 100644
index 0000000..9569a2e
--- /dev/null
+++ b/go.mod
@@ -0,0 +1,3 @@
+module github.com/feliixx/boa
+
+go 1.18
diff --git a/parser.go b/parser.go
new file mode 100644
index 0000000..f6e6290
--- /dev/null
+++ b/parser.go
@@ -0,0 +1,184 @@
+package boa
+
+import (
+	"encoding/json"
+	"fmt"
+	"io"
+	"reflect"
+	"strconv"
+	"strings"
+)
+
+var (
+	config   map[string]any
+	defaults = map[string]any{}
+)
+
+// ParseConfig reads the config from an io.Reader.
+func ParseConfig(jsonConfig io.Reader) error {
+
+	d := json.NewDecoder(jsonConfig)
+	d.UseNumber()
+
+	return d.Decode(&config)
+}
+
+// SetDefault set the default value for this key.
+func SetDefault(key string, value any) {
+
+	switch reflect.TypeOf(value).Kind() {
+
+	case reflect.Int,
+		reflect.Int32,
+		reflect.Int64,
+		reflect.Uint,
+		reflect.Uint32,
+		reflect.Uint64:
+		defaults[key] = json.Number(fmt.Sprintf("%d", value))
+
+	case reflect.Float64:
+		defaults[key] = json.Number(strconv.FormatFloat(value.(float64), 'g', -1, 64))
+	default:
+		defaults[key] = value
+	}
+
+}
+
+// GetString returns the value associated with the key as a string.
+func GetString(key string) string {
+	return cast[string](getValue(key))
+}
+
+// GetBool returns the value associated with the key as a bool.
+func GetBool(key string) bool {
+	return cast[bool](getValue(key))
+}
+
+// GetInt returns the value associated with the key as an int.
+func GetInt(key string) int {
+
+	n := cast[json.Number](getValue(key))
+	v, err := strconv.ParseInt(string(n), 10, 64)
+	if err != nil {
+		panic("Can't parse number '%s' as an int")
+	}
+	return int(v)
+}
+
+// GetInt32 returns the value associated with the key as an int32
+func GetInt32(key string) int32 {
+	n := cast[json.Number](getValue(key))
+	v, err := strconv.ParseInt(string(n), 10, 32)
+	if err != nil {
+		panic("Can't parse number '%s' as an int32")
+	}
+	return int32(v)
+}
+
+// GetInt64 returns the value associated with the key as an int64.
+func GetInt64(key string) int64 {
+	n := cast[json.Number](getValue(key))
+	v, err := strconv.ParseInt(string(n), 10, 64)
+	if err != nil {
+		panic("Can't parse number '%s' as an int64")
+	}
+	return v
+}
+
+// GetUint returns the value associated with the key as an uint.
+func GetUint(key string) uint {
+	n := cast[json.Number](getValue(key))
+	v, err := strconv.ParseUint(string(n), 10, 64)
+	if err != nil {
+		panic("Can't parse number '%s' as an uint")
+	}
+	return uint(v)
+}
+
+// GetUint32 returns the value associated with the key as an uint32.
+func GetUint32(key string) uint32 {
+	n := cast[json.Number](getValue(key))
+	v, err := strconv.ParseUint(string(n), 10, 32)
+	if err != nil {
+		panic("Can't parse number '%s' as an uint32")
+	}
+	return uint32(v)
+}
+
+// GetUint64 returns the value associated with the key as an uint64.
+func GetUint64(key string) uint64 {
+	n := cast[json.Number](getValue(key))
+	v, err := strconv.ParseUint(string(n), 10, 64)
+	if err != nil {
+		panic("Can't parse number '%s' as an uint64")
+	}
+	return uint64(v)
+}
+
+// GetFloat64 returns the value associated with the key as a float64.
+func GetFloat64(key string) float64 {
+	n := cast[json.Number](getValue(key))
+	v, err := strconv.ParseFloat(string(n), 64)
+	if err != nil {
+		panic("Can't parse number '%s' as a float64")
+	}
+	return v
+}
+
+// GetAny returns any value associated with the key.
+func GetAny(key string) any {
+	return getValue(key)
+}
+
+func getValue(key string) any {
+
+	nProp := strings.Split(key, ".")
+	nested := config
+
+	for i, prop := range nProp {
+
+		if i == len(nProp)-1 {
+
+			if v, ok := nested[prop]; ok {
+				return v
+			}
+
+			path := "'" + strings.Join(nProp[:i], ".") + "'"
+			if path == "''" {
+				path = "root object"
+			}
+
+			return getDefaultValueOrPanic(
+				key,
+				fmt.Sprintf("%s has no key '%s'", path, prop),
+			)
+		}
+
+		if _, ok := nested[prop].(map[string]any); !ok {
+			return getDefaultValueOrPanic(
+				key,
+				fmt.Sprintf("%s is not an object", strings.Join(nProp[:i+1], ".")),
+			)
+		}
+
+		nested = nested[prop].(map[string]any)
+	}
+
+	return nil
+}
+
+func getDefaultValueOrPanic(key, panicMsg string) any {
+	v, ok := defaults[key]
+	if !ok {
+		panic(panicMsg)
+	}
+	return v
+}
+
+func cast[T any](v any) T {
+	s, ok := v.(T)
+	if !ok {
+		panic(fmt.Sprintf("'%v' is not a %s", v, reflect.TypeOf(s)))
+	}
+	return s
+}
diff --git a/parser_test.go b/parser_test.go
new file mode 100644
index 0000000..621c380
--- /dev/null
+++ b/parser_test.go
@@ -0,0 +1,278 @@
+package boa_test
+
+import (
+	"strings"
+	"testing"
+
+	"github.com/feliixx/boa"
+)
+
+func TestGetString(t *testing.T) {
+
+	getStringTests := []struct {
+		name     string
+		config   string
+		property string
+		expected string
+		panic    bool
+		panicMsg string
+	}{
+		{
+			name:     "single key",
+			config:   `{ "key": "value" }`,
+			property: "key",
+			expected: "value",
+		},
+		{
+			name:     "non existing key",
+			config:   `{ "key": "value" }`,
+			property: "yellow",
+			panic:    true,
+			panicMsg: `root object has no key 'yellow'`,
+		},
+		{
+			name:     "one nested level",
+			config:   `{ "root": { "key": "value"}}`,
+			property: "root.key",
+			expected: "value",
+		},
+		{
+			name:     "non existing nested",
+			config:   `{"root": {"first": {"key":"value"}}}`,
+			property: "root.wrong.key",
+			panic:    true,
+			panicMsg: `root.wrong is not an object`,
+		},
+		{
+			name:     "non existing nested key",
+			config:   `{"root": {"first": {"key":"value"}}}`,
+			property: "root.first.yellow",
+			panic:    true,
+			panicMsg: `'root.first' has no key 'yellow'`,
+		},
+		{
+			name:     "wrong type",
+			config:   `{"root": {"first": {"key":true}}}`,
+			property: "root.first.key",
+			panic:    true,
+			panicMsg: `'true' is not a string`,
+		},
+	}
+
+	for _, test := range getStringTests {
+
+		tt := test
+		t.Run(tt.name, func(t *testing.T) {
+
+			if tt.panic {
+				defer func() {
+					panic := recover()
+					if panic == nil {
+						t.Error("test should have triggered a panic")
+					}
+
+					if panic != tt.panicMsg {
+						t.Errorf("expected '%s' but got '%s'", tt.panicMsg, panic)
+					}
+				}()
+			}
+
+			loadConfig(t, tt.config)
+
+			got := boa.GetString(tt.property)
+			if got != tt.expected {
+				t.Errorf("expected '%s' but got '%s'", tt.expected, got)
+			}
+		})
+	}
+}
+
+func TestAll(t *testing.T) {
+
+	config := `
+	{
+		"root": {
+			"string": "s",
+			"boolTrue": true,
+			"boolFalse": false,
+			"int": -1,
+			"int32": -32,
+			"int64": -64,
+			"uint": 1,
+			"uint32": 32,
+			"uint64": 64,
+			"float64": 1.23842323
+		}
+	}`
+
+	loadConfig(t, config)
+
+	tests := []struct {
+		name string
+		want any
+		got  any
+	}{
+		{
+			name: "string",
+			want: "s",
+			got:  boa.GetString("root.string"),
+		},
+		{
+			name: "bool true",
+			want: true,
+			got:  boa.GetBool("root.boolTrue"),
+		}, {
+			name: "bool false",
+			want: false,
+			got:  boa.GetBool("root.boolFalse"),
+		},
+		{
+			name: "int",
+			want: int(-1),
+			got:  boa.GetInt("root.int"),
+		},
+		{
+			name: "int32",
+			want: int32(-32),
+			got:  boa.GetInt32("root.int32"),
+		},
+		{
+			name: "int64",
+			want: int64(-64),
+			got:  boa.GetInt64("root.int64"),
+		},
+		{
+			name: "uint",
+			want: uint(1),
+			got:  boa.GetUint("root.uint"),
+		},
+		{
+			name: "uint32",
+			want: uint32(32),
+			got:  boa.GetUint32("root.uint32"),
+		},
+		{
+			name: "uint64",
+			want: uint64(64),
+			got:  boa.GetUint64("root.uint64"),
+		},
+		{
+			name: "float64",
+			want: 1.23842323,
+			got:  boa.GetFloat64("root.float64"),
+		},
+	}
+
+	for _, test := range tests {
+
+		tt := test
+		t.Run(tt.name, func(t *testing.T) {
+			if tt.want != tt.got {
+				t.Errorf("expected '%v' but got '%v'", tt.want, tt.got)
+			}
+		})
+	}
+}
+
+func TestSetDefaults(t *testing.T) {
+
+	config := `
+	{
+		"root": {
+			"string": "s"
+		}
+	}`
+
+	loadConfig(t, config)
+
+	boa.SetDefault("root.string", "unused")
+	boa.SetDefault("root.first", "first")
+	boa.SetDefault("root.first.second", "second")
+	boa.SetDefault("int", -12)
+	boa.SetDefault("int32", -2334)
+	boa.SetDefault("int64", -17286145274665)
+	boa.SetDefault("uint", 12)
+	boa.SetDefault("uint32", 2334)
+	boa.SetDefault("uint64", 17286145274665)
+	boa.SetDefault("float64", 0.72631524721)
+	boa.SetDefault("precision.float", 1563246315263.35152323132)
+
+	tests := []struct {
+		name string
+		want any
+		got  any
+	}{
+		{
+			name: "default not used if key exist",
+			want: "s",
+			got:  boa.GetString("root.string"),
+		},
+		{
+			name: "default used if key does not exist",
+			want: "first",
+			got:  boa.GetString("root.first"),
+		},
+		{
+			name: "default used if nested object doesn't exist",
+			want: "second",
+			got:  boa.GetString("root.first.second"),
+		},
+		{
+			name: "default int",
+			want: -12,
+			got:  boa.GetInt("int"),
+		},
+		{
+			name: "default int32",
+			want: int32(-2334),
+			got:  boa.GetInt32("int32"),
+		},
+		{
+			name: "default int64",
+			want: int64(-17286145274665),
+			got:  boa.GetInt64("int64"),
+		},
+		{
+			name: "default uint",
+			want: uint(12),
+			got:  boa.GetUint("uint"),
+		},
+		{
+			name: "default uint32",
+			want: uint32(2334),
+			got:  boa.GetUint32("uint32"),
+		},
+		{
+			name: "default uint64",
+			want: uint64(17286145274665),
+			got:  boa.GetUint64("uint64"),
+		},
+		{
+			name: "default float",
+			want: 0.72631524721,
+			got:  boa.GetFloat64("float64"),
+		},
+		{
+			name: "precision float",
+			want: 1563246315263.35152323132,
+			got:  boa.GetFloat64("precision.float"),
+		},
+	}
+
+	for _, test := range tests {
+
+		tt := test
+		t.Run(tt.name, func(t *testing.T) {
+			if tt.want != tt.got {
+				t.Errorf("expected '%v' but got '%v'", tt.want, tt.got)
+			}
+		})
+	}
+}
+
+func loadConfig(t *testing.T, config string) {
+	err := boa.ParseConfig(strings.NewReader(config))
+	if err != nil {
+		t.Error(err)
+	}
+}
diff --git a/test.sh b/test.sh
new file mode 100755
index 0000000..80d33f9
--- /dev/null
+++ b/test.sh
@@ -0,0 +1,12 @@
+#!/usr/bin/env bash
+
+set -e
+echo "" > coverage.txt
+
+for d in $(go list ./... | grep -v vendor); do
+    go test -race -coverprofile=profile.out -covermode=atomic $d
+    if [ -f profile.out ]; then
+        cat profile.out >> coverage.txt
+        rm profile.out
+    fi
+done
\ No newline at end of file