Repository: BurntSushi/toml
Pull request #298 (2021-06-20 19:37:09)

Use toml-test parser interface, move some tests to there
Expand on #297 by not calling the toml-test-* binaries with the Parser
interface added in https://github.com/BurntSushi/toml-test/pull/73

Also move a bunch of tests to toml-test.


Patch:
diff --git a/Makefile b/Makefile
deleted file mode 100644
index 2b86494..0000000
--- a/Makefile
+++ /dev/null
@@ -1,5 +0,0 @@
-all:
-	@e=0  # So it won't stop on the first command that fails.
-	@go install ./...
-	@go test ./... || e=1
-	@exit $e
diff --git a/decode.go b/decode.go
index ccaae7b..50a97fa 100644
--- a/decode.go
+++ b/decode.go
@@ -11,10 +11,6 @@ import (
 	"time"
 )
 
-func e(format string, args ...interface{}) error {
-	return fmt.Errorf("toml: "+format, args...)
-}
-
 // Unmarshaler is the interface implemented by objects that can unmarshal a
 // TOML description of themselves.
 type Unmarshaler interface {
@@ -142,7 +138,6 @@ func DecodeReader(r io.Reader, v interface{}) (MetaData, error) {
 // Any type mismatch produces an error. Finding a type that we don't know
 // how to handle produces an unsupported type error.
 func (md *MetaData) unify(data interface{}, rv reflect.Value) error {
-
 	// Special case. Look for a `Primitive` value.
 	// TODO: #76 would make this superfluous after implemented.
 	if rv.Type() == reflect.TypeOf((*Primitive)(nil)).Elem() {
@@ -496,6 +491,10 @@ func isUnifiable(rv reflect.Value) bool {
 	return false
 }
 
+func e(format string, args ...interface{}) error {
+	return fmt.Errorf("toml: "+format, args...)
+}
+
 func badtype(expected string, data interface{}) error {
 	return e("cannot load TOML value of type %T into a Go %s", data, expected)
 }
diff --git a/encode.go b/encode.go
index c6b05c8..4118841 100644
--- a/encode.go
+++ b/encode.go
@@ -17,19 +17,13 @@ import (
 type tomlEncodeError struct{ error }
 
 var (
-	errArrayMixedElementTypes = errors.New(
-		"toml: cannot encode array with mixed element types")
-	errArrayNilElement = errors.New(
-		"toml: cannot encode array with nil element")
-	errNonString = errors.New(
-		"toml: cannot encode a map with non-string key type")
-	errAnonNonStruct = errors.New(
-		"toml: cannot encode an anonymous field that is not a struct")
-	errArrayNoTable = errors.New(
-		"toml: TOML array element cannot contain a table")
-	errNoKey = errors.New(
-		"toml: top-level values must be Go maps or structs")
-	errAnything = errors.New("") // used in testing
+	errArrayMixedElementTypes = errors.New("toml: cannot encode array with mixed element types")
+	errArrayNilElement        = errors.New("toml: cannot encode array with nil element")
+	errNonString              = errors.New("toml: cannot encode a map with non-string key type")
+	errAnonNonStruct          = errors.New("toml: cannot encode an anonymous field that is not a struct")
+	errArrayNoTable           = errors.New("toml: TOML array element cannot contain a table")
+	errNoKey                  = errors.New("toml: top-level values must be Go maps or structs")
+	errAnything               = errors.New("") // used in testing
 )
 
 var quotedReplacer = strings.NewReplacer(
diff --git a/go.mod b/go.mod
index 2f0921e..e2eb626 100644
--- a/go.mod
+++ b/go.mod
@@ -2,4 +2,4 @@ module github.com/BurntSushi/toml
 
 go 1.13
 
-require github.com/BurntSushi/toml-test v0.1.1-0.20210619051135-67b199419f32
+require github.com/BurntSushi/toml-test v0.1.1-0.20210620192437-de01089bbf76
diff --git a/go.sum b/go.sum
index fa2ebc7..224037d 100644
--- a/go.sum
+++ b/go.sum
@@ -1,4 +1,4 @@
 github.com/BurntSushi/toml v0.3.2-0.20210614224209-34d990aa228d/go.mod h1:2QZjSXA5e+XyFeCAxxtL8Z4StYUsTquL8ODGPR3C3MA=
-github.com/BurntSushi/toml-test v0.1.1-0.20210619051135-67b199419f32 h1:h3GzZj1jBAKc2+lDW5UeN7LBSJLetljIUfjvWq+kpaA=
-github.com/BurntSushi/toml-test v0.1.1-0.20210619051135-67b199419f32/go.mod h1:P/PrhmZ37t5llHfDuiouWXtFgqOoQ12SAh9j6EjrBR4=
+github.com/BurntSushi/toml-test v0.1.1-0.20210620192437-de01089bbf76 h1:DXhME0XxteaM9BJ3zRPDAfNAW5hW0unQhkCxme8K0ms=
+github.com/BurntSushi/toml-test v0.1.1-0.20210620192437-de01089bbf76/go.mod h1:P/PrhmZ37t5llHfDuiouWXtFgqOoQ12SAh9j6EjrBR4=
 zgo.at/zli v0.0.0-20210619044753-e7020a328e59/go.mod h1:HLAc12TjNGT+VRXr76JnsNE3pbooQtwKWhX+RlDjQ2Y=
diff --git a/lex.go b/lex.go
index 6065c86..de79168 100644
--- a/lex.go
+++ b/lex.go
@@ -139,7 +139,7 @@ func (lx *lexer) emitTrim(typ itemType) {
 
 func (lx *lexer) next() (r rune) {
 	if lx.atEOF {
-		panic("next called after EOF")
+		panic("BUG in lexer: next called after EOF")
 	}
 	if lx.pos >= len(lx.input) {
 		lx.atEOF = true
@@ -179,7 +179,7 @@ func (lx *lexer) backup() {
 		return
 	}
 	if lx.nprev < 1 {
-		panic("backed up too far")
+		panic("BUG in lexer: backed up too far")
 	}
 	w := lx.prevWidths[0]
 	lx.prevWidths[0] = lx.prevWidths[1]


Test patch:
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index ccde8a3..028699f 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -1,22 +1,30 @@
-on: ['push', 'pull_request']
-name: 'go test'
-
-jobs:
-  test:
-    strategy:
-      matrix:
-        go-version: ['1.13.x', '1.14.x', '1.15.x', '1.16.x']
-        os:         ['ubuntu-latest', 'macos-latest', 'windows-latest']
-    runs-on: '${{ matrix.os }}'
-    steps:
-
-    - name: 'Install Go'
-      uses: 'actions/setup-go@v2'
-      with:
-        go-version: '${{ matrix.go-version }}'
-
-    - name: 'Checkout code'
-      uses: 'actions/checkout@v2'
-
-    - name: 'Test'
-      run: 'make'
+{
+  "name": "go test",
+  "on":   ["push", "pull_request"],
+  "jobs": {
+    "test": {
+      "strategy": {
+        "matrix": {
+          "go-version": ["1.13.x", "1.14.x", "1.15.x", "1.16.x"],
+          "os":         ["ubuntu-latest", "macos-latest", "windows-latest"]
+        }
+      },
+      "runs-on": "${{ matrix.os }}",
+      "steps": [
+        {
+          "name": "Install Go",
+          "uses": "actions/setup-go@v2",
+          "with": {"go-version": "${{ matrix.go-version }}"}
+        },
+        {
+          "name": "Checkout code",
+          "uses": "actions/checkout@v2"
+        },
+        {
+          "name": "Test",
+          "run": "go test -v ./..."
+        }
+      ]
+    }
+  }
+}
diff --git a/cmd/toml-test-decoder/main.go b/cmd/toml-test-decoder/main.go
index 10e246f..e1f5491 100644
--- a/cmd/toml-test-decoder/main.go
+++ b/cmd/toml-test-decoder/main.go
@@ -39,12 +39,12 @@ func main() {
 
 	j := json.NewEncoder(os.Stdout)
 	j.SetIndent("", "  ")
-	if err := j.Encode(translate(decoded)); err != nil {
+	if err := j.Encode(addJSONTags(decoded)); err != nil {
 		log.Fatalf("Error encoding JSON: %s", err)
 	}
 }
 
-func translate(tomlData interface{}) interface{} {
+func addJSONTags(tomlData interface{}) interface{} {
 	switch orig := tomlData.(type) {
 	default:
 		panic(fmt.Sprintf("Unknown type: %T", tomlData))
@@ -52,19 +52,19 @@ func translate(tomlData interface{}) interface{} {
 	case map[string]interface{}:
 		typed := make(map[string]interface{}, len(orig))
 		for k, v := range orig {
-			typed[k] = translate(v)
+			typed[k] = addJSONTags(v)
 		}
 		return typed
 	case []map[string]interface{}:
 		typed := make([]map[string]interface{}, len(orig))
 		for i, v := range orig {
-			typed[i] = translate(v).(map[string]interface{})
+			typed[i] = addJSONTags(v).(map[string]interface{})
 		}
 		return typed
 	case []interface{}:
 		typed := make([]interface{}, len(orig))
 		for i, v := range orig {
-			typed[i] = translate(v)
+			typed[i] = addJSONTags(v)
 		}
 		return typed
 	case time.Time:
diff --git a/cmd/toml-test-encoder/main.go b/cmd/toml-test-encoder/main.go
index ebb0497..02a2378 100644
--- a/cmd/toml-test-encoder/main.go
+++ b/cmd/toml-test-encoder/main.go
@@ -36,12 +36,12 @@ func main() {
 		log.Fatalf("Error decoding JSON: %s", err)
 	}
 
-	if err := toml.NewEncoder(os.Stdout).Encode(translate(tmp)); err != nil {
+	if err := toml.NewEncoder(os.Stdout).Encode(removeJSONTags(tmp)); err != nil {
 		log.Fatalf("Error encoding TOML: %s", err)
 	}
 }
 
-func translate(typedJson interface{}) interface{} {
+func removeJSONTags(typedJson interface{}) interface{} {
 	switch v := typedJson.(type) {
 	case map[string]interface{}:
 		if len(v) == 2 && in("type", v) && in("value", v) {
@@ -49,13 +49,13 @@ func translate(typedJson interface{}) interface{} {
 		}
 		m := make(map[string]interface{}, len(v))
 		for k, v2 := range v {
-			m[k] = translate(v2)
+			m[k] = removeJSONTags(v2)
 		}
 		return m
 	case []interface{}:
 		a := make([]interface{}, len(v))
 		for i := range v {
-			a[i] = translate(v[i])
+			a[i] = removeJSONTags(v[i])
 		}
 		return a
 	}
diff --git a/decode_test.go b/decode_test.go
index fb29e75..37e2d7f 100644
--- a/decode_test.go
+++ b/decode_test.go
@@ -3,120 +3,62 @@ package toml
 import (
 	"errors"
 	"fmt"
-	"math"
+	"io/ioutil"
+	"os"
 	"reflect"
 	"strings"
 	"testing"
 	"time"
 )
 
-func TestDecodeSimple(t *testing.T) {
-	var testSimple = `
-age = 250
-andrew = "gallant"
-kait = "brady"
-now = 1987-07-05T05:45:00Z
-nowEast = 2017-06-22T16:15:21+08:00
-nowWest = 2017-06-22T02:14:36-06:00
-yesOrNo = true
-pi = 3.14
-colors = [
-	["red", "green", "blue"],
-	["cyan", "magenta", "yellow", "black"],
-]
-
-[My.Cats]
-plato = "cat 1"
-cauchy = "cat 2"
-`
-
-	type cats struct {
-		Plato  string
-		Cauchy string
+func TestDecodeReader(t *testing.T) {
+	var i struct{ A int }
+	meta, err := DecodeReader(strings.NewReader("a = 42"), &i)
+	if err != nil {
+		t.Fatal(err)
 	}
-	type simple struct {
-		Age     int
-		Colors  [][]string
-		Pi      float64
-		YesOrNo bool
-		Now     time.Time
-		NowEast time.Time
-		NowWest time.Time
-		Andrew  string
-		Kait    string
-		My      map[string]cats
+	have := fmt.Sprintf("%v %v %v", i, meta.Keys(), meta.Type("a"))
+	want := "{42} [a] Integer"
+	if have != want {
+		t.Errorf("\nhave: %s\nwant: %s", have, want)
 	}
+}
 
-	var val simple
-	_, err := Decode(testSimple, &val)
+func TestDecodeFile(t *testing.T) {
+	tmp, err := ioutil.TempFile("", "toml-")
 	if err != nil {
 		t.Fatal(err)
 	}
-
-	now, err := time.Parse("2006-01-02T15:04:05", "1987-07-05T05:45:00")
-	if err != nil {
-		panic(err)
+	defer os.Remove(tmp.Name())
+	if _, err := tmp.WriteString("a = 42"); err != nil {
+		t.Fatal(err)
 	}
-	nowEast, err := time.Parse("2006-01-02T15:04:05-07:00", "2017-06-22T16:15:21+08:00")
-	if err != nil {
-		panic(err)
+	if err := tmp.Close(); err != nil {
+		t.Fatal(err)
 	}
-	nowWest, err := time.Parse("2006-01-02T15:04:05-07:00", "2017-06-22T02:14:36-06:00")
+
+	var i struct{ A int }
+	meta, err := DecodeFile(tmp.Name(), &i)
 	if err != nil {
-		panic(err)
-	}
-	var answer = simple{
-		Age:     250,
-		Andrew:  "gallant",
-		Kait:    "brady",
-		Now:     now,
-		NowEast: nowEast,
-		NowWest: nowWest,
-		YesOrNo: true,
-		Pi:      3.14,
-		Colors: [][]string{
-			{"red", "green", "blue"},
-			{"cyan", "magenta", "yellow", "black"},
-		},
-		My: map[string]cats{
-			"Cats": {Plato: "cat 1", Cauchy: "cat 2"},
-		},
+		t.Fatal(err)
 	}
-	if !reflect.DeepEqual(val, answer) {
-		t.Fatalf("Expected\n-----\n%#v\n-----\nbut got\n-----\n%#v\n",
-			answer, val)
+	have := fmt.Sprintf("%v %v %v", i, meta.Keys(), meta.Type("a"))
+	want := "{42} [a] Integer"
+	if have != want {
+		t.Errorf("\nhave: %s\nwant: %s", have, want)
 	}
 }
 
-func TestUTF16(t *testing.T) {
-	tests := []struct {
-		in      []byte
-		wantErr string
-	}{
-		// a = "b" in UTF-16, without BOM and with the LE and BE BOMs.
-		{
-			[]byte{0x61, 0x00, 0x20, 0x00, 0x3d, 0x00, 0x20, 0x00, 0x22, 0x00, 0x62, 0x00, 0x22, 0x00, 0x0a, 0x00},
-			`files cannot contain NULL bytes; probably using UTF-16; TOML files must be UTF-8`,
-		},
-		{
-			[]byte{0xfe, 0xff, 0x61, 0x00, 0x20, 0x00, 0x3d, 0x00, 0x20, 0x00, 0x22, 0x00, 0x62, 0x00, 0x22, 0x00, 0x0a, 0x00},
-			`files cannot contain NULL bytes; probably using UTF-16; TOML files must be UTF-8`,
-		},
-		//  UTF-8 with BOM
-		{[]byte("\xff\xfea = \"b\""), ``},
-		{[]byte("\xfe\xffa = \"b\""), ``},
-	}
-
-	for _, tt := range tests {
+func TestDecodeBOM(t *testing.T) {
+	for _, tt := range [][]byte{
+		[]byte("\xff\xfea = \"b\""),
+		[]byte("\xfe\xffa = \"b\""),
+	} {
 		t.Run("", func(t *testing.T) {
 			var s struct{ A string }
-
-			_, err := Decode(string(tt.in), &s)
-			if !errorContains(err, tt.wantErr) {
-				t.Fatalf("wrong error\nhave: %q\nwant: %q", err, tt.wantErr)
-			}
-			if tt.wantErr != "" {
-				return
+			_, err := Decode(string(tt), &s)
+			if err != nil {
+				t.Fatal(err)
 			}
 			if s.A != "b" {
 				t.Errorf(`s.A is not "b" but %q`, s.A)
@@ -178,15 +120,14 @@ func TestDecodeEmbedded(t *testing.T) {
 	}
 }
 
-func TestDecodeIgnoredFields(t *testing.T) {
-	type simple struct {
-		Number int `toml:"-"`
-	}
+func TestDecodeIgnoreFields(t *testing.T) {
 	const input = `
 Number = 123
 - = 234
 `
-	var s simple
+	var s struct {
+		Number int `toml:"-"`
+	}
 	if _, err := Decode(input, &s); err != nil {
 		t.Fatal(err)
 	}
@@ -195,7 +136,7 @@ Number = 123
 	}
 }
 
-func TestTableArrays(t *testing.T) {
+func TestDecodeTableArrays(t *testing.T) {
 	var tomlTableArrays = `
 [[albums]]
 name = "Born to Run"
@@ -242,120 +183,7 @@ name = "Born in the USA"
 	}
 }
 
-func TestTableNesting(t *testing.T) {
-	for _, tt := range []struct {
-		t    string
-		want []string
-	}{
-		{"[a.b.c]", []string{"a", "b", "c"}},
-		{`[a."b.c"]`, []string{"a", "b.c"}},
-		{`[a.'b.c']`, []string{"a", "b.c"}},
-		{`[a.' b ']`, []string{"a", " b "}},
-		{"[ d.e.f ]", []string{"d", "e", "f"}},
-		{"[ g . h . i ]", []string{"g", "h", "i"}},
-		{`[ j . "ʞ" . 'l' ]`, []string{"j", "ʞ", "l"}},
-	} {
-		var m map[string]interface{}
-		if _, err := Decode(tt.t, &m); err != nil {
-			t.Errorf("Decode(%q): got error: %s", tt.t, err)
-			continue
-		}
-		if keys := extractNestedKeys(m); !reflect.DeepEqual(keys, tt.want) {
-			t.Errorf("Decode(%q): got nested keys %#v; want %#v",
-				tt.t, keys, tt.want)
-		}
-	}
-}
-
-func extractNestedKeys(v map[string]interface{}) []string {
-	var result []string
-	for {
-		if len(v) != 1 {
-			return result
-		}
-		for k, m := range v {
-			result = append(result, k)
-			var ok bool
-			v, ok = m.(map[string]interface{})
-			if !ok {
-				return result
-			}
-		}
-
-	}
-}
-
-// Case insensitive matching tests.
-// A bit more comprehensive than needed given the current implementation,
-// but implementations change.
-// Probably still missing demonstrations of some ugly corner cases regarding
-// case insensitive matching and multiple fields.
-func TestCase(t *testing.T) {
-	var caseToml = `
-tOpString = "string"
-tOpInt = 1
-tOpFloat = 1.1
-tOpBool = true
-tOpdate = 2006-01-02T15:04:05Z
-tOparray = [ "array" ]
-Match = "i should be in Match only"
-MatcH = "i should be in MatcH only"
-once = "just once"
-[nEst.eD]
-nEstedString = "another string"
-`
-
-	type InsensitiveEd struct {
-		NestedString string
-	}
-
-	type InsensitiveNest struct {
-		Ed InsensitiveEd
-	}
-
-	type Insensitive struct {
-		TopString string
-		TopInt    int
-		TopFloat  float64
-		TopBool   bool
-		TopDate   time.Time
-		TopArray  []string
-		Match     string
-		MatcH     string
-		Once      string
-		OncE      string
-		Nest      InsensitiveNest
-	}
-
-	tme, err := time.Parse(time.RFC3339, time.RFC3339[:len(time.RFC3339)-5])
-	if err != nil {
-		panic(err)
-	}
-	expected := Insensitive{
-		TopString: "string",
-		TopInt:    1,
-		TopFloat:  1.1,
-		TopBool:   true,
-		TopDate:   tme,
-		TopArray:  []string{"array"},
-		MatcH:     "i should be in MatcH only",
-		Match:     "i should be in Match only",
-		Once:      "just once",
-		OncE:      "",
-		Nest: InsensitiveNest{
-			Ed: InsensitiveEd{NestedString: "another string"},
-		},
-	}
-	var got Insensitive
-	if _, err := Decode(caseToml, &got); err != nil {
-		t.Fatal(err)
-	}
-	if !reflect.DeepEqual(expected, got) {
-		t.Fatalf("\n%#v\n!=\n%#v\n", expected, got)
-	}
-}
-
-func TestPointers(t *testing.T) {
+func TestDecodePointers(t *testing.T) {
 	type Object struct {
 		Type        string
 		Description string
@@ -404,63 +232,9 @@ Description = "da base"
 	}
 }
 
-func TestDecodeDatetime(t *testing.T) {
-	tz7 := time.FixedZone("", -3600*7)
-
-	for _, tt := range []struct {
-		in   string
-		want time.Time
-	}{
-		// Offset datetime
-		{"1979-05-27T07:32:00Z", time.Date(1979, 05, 27, 07, 32, 0, 0, time.UTC)},
-		{"1979-05-27T07:32:00.999999Z", time.Date(1979, 05, 27, 07, 32, 0, 999999000, time.UTC)},
-		{"1979-05-27T00:32:00-07:00", time.Date(1979, 05, 27, 00, 32, 0, 0, tz7)},
-		{"1979-05-27T00:32:00.999999-07:00", time.Date(1979, 05, 27, 00, 32, 0, 999999000, tz7)},
-		{"1979-05-27T00:32:00.24-07:00", time.Date(1979, 05, 27, 00, 32, 0, 240000000, tz7)},
-		{"1979-05-27 07:32:00Z", time.Date(1979, 05, 27, 07, 32, 0, 0, time.UTC)},
-		{"1979-05-27t07:32:00z", time.Date(1979, 05, 27, 07, 32, 0, 0, time.UTC)},
-
-		// Local datetime; according to the spec this should be "without any
-		// relation to an offset or timezone. It cannot be converted to an
-		// instant in time without additional information. Conversion to an
-		// instant, if required, is implementation-specific."
-		//
-		// Go doesn't supporting a time without a timezone, so use time.Local.
-		{"1979-05-27T07:32:00", time.Date(1979, 05, 27, 07, 32, 0, 0, time.Local)},
-		{"1979-05-27T07:32:00.999999", time.Date(1979, 05, 27, 07, 32, 0, 999999000, time.Local)},
-		{"1979-05-27T07:32:00.25", time.Date(1979, 05, 27, 07, 32, 0, 250000000, time.Local)},
-
-		{"1979-05-27", time.Date(1979, 05, 27, 0, 0, 0, 0, time.Local)},
-
-		{"07:32:00", time.Date(0, 1, 1, 07, 32, 0, 0, time.Local)},
-		{"07:32:00.999999", time.Date(0, 1, 1, 07, 32, 0, 999999000, time.Local)},
-
-		// Make sure the space between the datetime and "#" isn't lexed.
-		{"1979-05-27T07:32:12-07:00  # c", time.Date(1979, 05, 27, 07, 32, 12, 0, tz7)},
-	} {
-		t.Run(tt.in, func(t *testing.T) {
-			var x struct{ D time.Time }
-			input := "d = " + tt.in
-			if _, err := Decode(input, &x); err != nil {
-				t.Fatalf("got error: %s", err)
-			}
-
-			if h, w := x.D.Format(time.RFC3339Nano), tt.want.Format(time.RFC3339Nano); h != w {
-				t.Errorf("\nhave: %s\nwant: %s", h, w)
-			}
-		})
-	}
-}
-
 func TestDecodeBadDatetime(t *testing.T) {
 	var x struct{ T time.Time }
-	for _, s := range []string{
-		"123",
-		"1230",
-		"2006-01-50T00:00:00Z",
-		"2006-01-30T00:00",
-		"2006-01-30T",
-	} {
+	for _, s := range []string{"123", "1230"} {
 		input := "T = " + s
 		if _, err := Decode(input, &x); err == nil {
 			t.Errorf("Expected invalid DateTime error for %q", s)
@@ -473,13 +247,6 @@ type sphere struct {
 	Radius float64
 }
 
-func TestDecodeSimpleArray(t *testing.T) {
-	var s1 sphere
-	if _, err := Decode(`center = [0.0, 1.5, 0.0]`, &s1); err != nil {
-		t.Fatal(err)
-	}
-}
-
 func TestDecodeArrayWrongSize(t *testing.T) {
 	var s1 sphere
 	if _, err := Decode(`center = [0.1, 2.3]`, &s1); err == nil {
@@ -487,7 +254,7 @@ func TestDecodeArrayWrongSize(t *testing.T) {
 	}
 }
 
-func TestDecodeLargeIntoSmallInt(t *testing.T) {
+func TestDecodeIntOverflow(t *testing.T) {
 	type table struct {
 		Value int8
 	}
@@ -532,146 +299,6 @@ func TestDecodeSizedInts(t *testing.T) {
 	}
 }
 
-func TestDecodeInts(t *testing.T) {
-	for _, tt := range []struct {
-		s    string
-		want int64
-	}{
-		{"0", 0},
-		{"0x0", 0},
-		{"0x00", 0},
-		{"0o0", 0},
-		{"0o00", 0},
-		{"0b0", 0},
-		{"0b00", 0},
-		{"+0", 0},
-		{"-0", 0},
-		{"+99", 99},
-		{"-10", -10},
-		{"1_234_567", 1234567},
-		{"1_2_3_4", 1234},
-		{"0xdead_BEEF", 0xdeadbeef},
-		{"0b0_1_1_0", 0b0110},
-		{"0o7_7_7", 0o777},
-		{"0x12345", 0x12345},
-		{"0x0987", 0x987},
-		{"0b1101", 0xd},
-		{"0o777", 0x1ff},
-		{"-9_223_372_036_854_775_808", math.MinInt64},
-		{"9_223_372_036_854_775_807", math.MaxInt64},
-	} {
-		var x struct{ N int64 }
-		input := "n = " + tt.s
-		if _, err := Decode(input, &x); err != nil {
-			t.Errorf("Decode(%q): got error: %s", input, err)
-			continue
-		}
-		if x.N != tt.want {
-			t.Errorf("Decode(%q): got %d; want %d", input, x.N, tt.want)
-		}
-	}
-}
-
-func TestDecodeFloats(t *testing.T) {
-	for _, tt := range []struct {
-		s    string
-		want float64
-	}{
-		{"+0.0", 0},
-		{"-0.0", 0},
-		{"+1.0", 1},
-		{"3.1415", 3.1415},
-		{"-0.01", -0.01},
-		{"0.1", 0.1},
-		{"5e+22", 5e22},
-		{"1e6", 1e6},
-		{"1e06", 1e6},
-		{"1e006", 1e6},
-		{"-2E-2", -2e-2},
-		{"6.626e-34", 6.626e-34},
-		{"9_224_617.445_991_228_313", 9224617.445991228313},
-		{"9_876.54_32e1_0", 9876.5432e10},
-		{"inf", math.Inf(0)},
-		{"+inf", math.Inf(1)},
-		{"-inf", math.Inf(-1)},
-		{"nan", math.NaN()},
-		{"+nan", math.NaN()},
-		{"-nan", math.NaN()},
-	} {
-		t.Run(tt.s, func(t *testing.T) {
-			var x struct{ N float64 }
-			input := "n = " + tt.s
-			if _, err := Decode(input, &x); err != nil {
-				t.Fatalf("got error: %s", err)
-			}
-			if math.IsNaN(tt.want) {
-				if !math.IsNaN(x.N) {
-					t.Errorf("not NaN: %f", x.N)
-				}
-				return
-			}
-			if x.N != tt.want {
-				t.Errorf("got %f; want %f", x.N, tt.want)
-			}
-		})
-	}
-}
-
-func TestDecodeMalformedNumbers(t *testing.T) {
-	for _, tt := range []struct {
-		s    string
-		want string
-	}{
-		{"++99", "expected a digit"},
-		{"0..1", "must be followed by one or more digits"},
-		{"0.1.2", "Invalid float value"},
-		{"1e2.3", "Invalid float value"},
-		{"1e2e3", "Invalid float value"},
-		{"_123", "expected value"},
-		{"123_", "surrounded by digits"},
-		{"0b0_", "surrounded by digits"},
-		{"1._23", "surrounded by digits"},
-		{"1e__23", "surrounded by digits"},
-		{"123.", "must be followed by one or more digits"},
-		{"1.e2", "must be followed by one or more digits"},
-		{"00", "cannot have leading zeroes"},
-		{"01", "cannot have leading zeroes"},
-		{"+01", "cannot have leading zeroes"},
-		{"-01", "cannot have leading zeroes"},
-		{"01.2", "cannot have leading zeroes"},
-		{"-01.2", "cannot have leading zeroes"},
-		{"+01.2", "cannot have leading zeroes"},
-		{"0x_d00d", "not a hexidecimal number: '0x_'"},
-		{"0b_0", "not a binary number: '0b_'"},
-		{"0z", "but got 'z' instead"},
-		{"+0x3", "cannot use sign with non-decimal numbers: '+0x'"},
-		{"-0xf00", "cannot use sign with non-decimal numbers: '-0x'"},
-		{"0B0", "got 'B' instead"},
-		{"0X0", "got 'X' instead"},
-		{"0O0", "got 'O' instead"},
-		{"in", "expected value"},
-		{"+in", "invalid float: '+in'"},
-		{"-in", "invalid float: '-in'"},
-		{"na", "expected value"},
-		{"+na", "invalid float: '+na'"},
-		{"-na", "invalid float: '-na'"},
-		{"na_n", "expected value"},
-		{"+i_inf", "invalid float: '+i'"},
-	} {
-		t.Run(tt.s, func(t *testing.T) {
-			var x struct{ N interface{} }
-			input := "n = " + tt.s
-			_, err := Decode(input, &x)
-			if err == nil {
-				t.Fatalf("got nil, want error containing %q", tt.want)
-			}
-			if !strings.Contains(err.Error(), tt.want) {
-				t.Errorf("\nhave: %q\nwant: %q", err, tt.want)
-			}
-		})
-	}
-}
-
 func TestDecodeTypes(t *testing.T) {
 	type mystr string
 
@@ -697,7 +324,6 @@ func TestDecodeTypes(t *testing.T) {
 }
 
 func TestUnmarshaler(t *testing.T) {
-
 	var tomlBlob = `
 [dishes.hamboogie]
 name = "Hamboogie with fries"
@@ -857,32 +483,6 @@ points = [ { x = 1, y = 2, z = 3 },
 	}
 }
 
-func TestDecodeMalformedInlineTable(t *testing.T) {
-	for _, tt := range []struct {
-		s    string
-		want string
-	}{
-		{"{,}", "unexpected comma"},
-		{"{x = 3 y = 4}", "expected a comma or an inline table terminator"},
-		{"{x=3,,y=4}", "unexpected comma"},
-		{"{x=3,\ny=4}", "newlines not allowed"},
-		{"{x=3\n,y=4}", "newlines not allowed"},
-	} {
-		var x struct{ A map[string]int }
-		input := "a = " + tt.s
-		_, err := Decode(input, &x)
-		if err == nil {
-			t.Errorf("Decode(%q): got nil, want error containing %q",
-				input, tt.want)
-			continue
-		}
-		if !strings.Contains(err.Error(), tt.want) {
-			t.Errorf("Decode(%q): got %q, want error containing %q",
-				input, err, tt.want)
-		}
-	}
-}
-
 type menu struct {
 	Dishes map[string]dish
 }
@@ -1015,154 +615,6 @@ func TestDecodePrimitive(t *testing.T) {
 	}
 }
 
-func TestDecodeErrors(t *testing.T) {
-	tests := []struct {
-		in      string
-		wantErr string
-		lastKey bool
-	}{
-		{`x`, "unexpected EOF; expected key separator '='", false},
-		{`x  `, "unexpected EOF; expected key separator '='", true},
-		{`x="`, `unexpected EOF; expected '"'`, true},
-		{`x="""`, `unexpected EOF; expected '"""'`, true},
-		{`x='`, `unexpected EOF; expected "'"`, true},
-		{`x='''`, `unexpected EOF; expected "'''"`, true},
-		{"x = ", "unexpected EOF; expected value", true},
-		{"x = \n", "expected value but found '\\n' instead", true},
-
-		// Cases found by fuzzing in #155 and #239
-		{`""` + "\ufffd", "invalid UTF-8", false},
-		{`""=` + "\ufffd", "invalid UTF-8", false},
-		{`x="""`, "unexpected EOF", true},
-		{`x = [{ key = 42 #`, "expected a comma or an inline table terminator", true},
-		{`x = {a = 42 #`, "expected a comma or an inline table terminator '}', but got end of file instead", true},
-		{`x = [42 #`, "expected a comma or array terminator ']', but got end of file instead", false},
-
-		// Literal escape characters are not alllowed in any strings
-		{`x = """` + "\r" + `"""`, `control characters are not allowed`, true},
-		{`x = """` + "\x01" + `"""`, `control characters are not allowed`, true},
-		{`x = '''` + "\r" + `'''`, `control characters are not allowed`, true},
-		{`x = '''` + "\x01" + `'''`, `control characters are not allowed`, true},
-		{`x = "` + "\r" + `"`, `control characters are not allowed`, true},
-		{`x = "` + "\x01" + `"`, `control characters are not allowed`, true},
-		{`x = '` + "\r" + `'`, `control characters are not allowed`, true},
-		{`x = '` + "\x01" + `'`, `control characters are not allowed`, true},
-	}
-
-	for _, tt := range tests {
-		t.Run("", func(t *testing.T) {
-			var x struct{}
-			_, err := Decode(tt.in, &x)
-			if err == nil {
-				t.Fatal("err is nil")
-			}
-			if !errorContains(err, tt.wantErr) {
-				t.Errorf("wrong error\nhave: %q\nwant: %q", err, tt.wantErr)
-			}
-			if tt.lastKey && !errorContains(err, "last key parsed 'x") {
-				t.Errorf("last key parsed not in error\nhave: %q\nwant: %q", err, "last key parsed 'x'")
-			}
-		})
-	}
-}
-
-func TestDecodeMultilineNewlines(t *testing.T) {
-	tests := []struct {
-		in   string
-		want string
-	}{
-		// Note "NL" gets replaced by "\n" and "\r\n" (the tests are run twice);
-		// this makes it easier to read and write these tests.
-
-		{`x = """"""`, ``},
-		{`x = """\NL"""`, ``},       // Empty string
-		{`x = """\NL\NL\NL"""`, ``}, // Empty string
-
-		{`x = """a\NL    u2222b"""`, `au2222b`},     // Remove all whitespace after \
-		{`x = """a\NLNLNLu2222b"""`, `au2222b`},     // Remove all newlines
-		{`x = """a  \NL    u2222b"""`, `a  u2222b`}, // Don't remove whitespace before \
-
-		{`x = """a \ NLb"""`, `a b`}, // Allow any whitespace between \n and \
-		{`x = """a  \ NL b"""`, `a  b`},
-		{`x = """a \ 		    NLb"""`, `a b`},
-
-		{`x="""a\NLu2222b"""`, `au2222b`},        // Ends in \ → remove
-		{`x="""a\\NLu2222b"""`, `a\NLu2222b`},    // Ends in \\ → literal backslash, so keep NL.
-		{`x="""a\\\NLu2222b"""`, `a\u2222b`},     // Ends in \\\ → backslash followed by NL escape, so remove.
-		{`x="""a\\\\NLu2222b"""`, `a\\NLu2222b`}, // Ends in \\\\ → two lieral backslashes; keep NL
-
-		{`x = """NLa b \n cNLd e fNL"""`, "a b \n c\nd e f\n"},
-		{`x = """a b c\NL"""`, "a b c"},
-
-		{`x = """NLThe quick brown \NLNLNLfox jumps over \NL    the lazy dog."""`,
-			`The quick brown fox jumps over the lazy dog.`},
-		{`x = """\NL        The quick brown \NLNLNL        fox jumps over \NL        the lazy dog.\NL        """`,
-			`The quick brown fox jumps over the lazy dog.`},
-	}
-
-	replUnix := strings.NewReplacer("NL", "\n")
-	replWin := strings.NewReplacer("NL", "\r\n")
-	for _, tt := range tests {
-		t.Run("", func(t *testing.T) {
-			t.Run("unix", func(t *testing.T) {
-				in := replUnix.Replace(tt.in)
-				want := replUnix.Replace(tt.want)
-
-				var s struct{ X string }
-				_, err := Decode(in, &s)
-				if err != nil {
-					t.Fatal(err)
-				}
-				if s.X != want {
-					t.Errorf("\nhave: %q\nwant: %q", s.X, want)
-				}
-			})
-
-			t.Run("windows", func(t *testing.T) {
-				in := replWin.Replace(tt.in)
-				want := replWin.Replace(tt.want)
-
-				var s struct{ X string }
-				_, err := Decode(in, &s)
-				if err != nil {
-					t.Fatal(err)
-				}
-				if s.X != want {
-					t.Errorf("\nhave: %q\nwant: %q", s.X, want)
-				}
-			})
-		})
-	}
-}
-
-// Test for https://github.com/BurntSushi/toml/pull/166.
-func TestDecodeBoolArray(t *testing.T) {
-	for _, tt := range []struct {
-		s    string
-		got  interface{}
-		want interface{}
-	}{
-		{
-			"a = [true, false]",
-			&struct{ A []bool }{},
-			&struct{ A []bool }{[]bool{true, false}},
-		},
-		{
-			"a = {a = true, b = false}",
-			&struct{ A map[string]bool }{},
-			&struct{ A map[string]bool }{map[string]bool{"a": true, "b": false}},
-		},
-	} {
-		if _, err := Decode(tt.s, tt.got); err != nil {
-			t.Errorf("Decode(%q): %s", tt.s, err)
-			continue
-		}
-		if !reflect.DeepEqual(tt.got, tt.want) {
-			t.Errorf("Decode(%q): got %#v; want %#v", tt.s, tt.got, tt.want)
-		}
-	}
-}
-
 func BenchmarkDecode(b *testing.B) {
 	var testSimple = `
 age = 250
diff --git a/encode_test.go b/encode_test.go
index 73c8cdd..542f31f 100644
--- a/encode_test.go
+++ b/encode_test.go
@@ -58,352 +58,6 @@ func TestEncodeRoundTrip(t *testing.T) {
 	}
 }
 
-// XXX(burntsushi)
-// I think these tests probably should be removed. They are good, but they
-// ought to be obsolete by toml-test.
-func TestEncode(t *testing.T) {
-	type Embedded struct {
-		Int int `toml:"_int"`
-	}
-	type NonStruct int
-
-	date := time.Date(2014, 5, 11, 19, 30, 40, 0, time.UTC)
-	dateStr := "2014-05-11T19:30:40Z"
-
-	tests := map[string]struct {
-		input      interface{}
-		wantOutput string
-		wantError  error
-	}{
-		"bool field": {
-			input: struct {
-				BoolTrue  bool
-				BoolFalse bool
-			}{true, false},
-			wantOutput: "BoolTrue = true\nBoolFalse = false\n",
-		},
-		"int fields": {
-			input: struct {
-				Int   int
-				Int8  int8
-				Int16 int16
-				Int32 int32
-				Int64 int64
-			}{1, 2, 3, 4, 5},
-			wantOutput: "Int = 1\nInt8 = 2\nInt16 = 3\nInt32 = 4\nInt64 = 5\n",
-		},
-		"uint fields": {
-			input: struct {
-				Uint   uint
-				Uint8  uint8
-				Uint16 uint16
-				Uint32 uint32
-				Uint64 uint64
-			}{1, 2, 3, 4, 5},
-			wantOutput: "Uint = 1\nUint8 = 2\nUint16 = 3\nUint32 = 4" +
-				"\nUint64 = 5\n",
-		},
-		"float fields": {
-			input: struct {
-				Float32 float32
-				Float64 float64
-			}{1.5, 2.5},
-			wantOutput: "Float32 = 1.5\nFloat64 = 2.5\n",
-		},
-		"string field": {
-			input:      struct{ String string }{"foo"},
-			wantOutput: "String = \"foo\"\n",
-		},
-		"string field with \\n escape": {
-			input:      struct{ String string }{"foo\n"},
-			wantOutput: "String = \"foo\\n\"\n",
-		},
-		"string field and unexported field": {
-			input: struct {
-				String     string
-				unexported int
-			}{"foo", 0},
-			wantOutput: "String = \"foo\"\n",
-		},
-		"datetime field in UTC": {
-			input:      struct{ Date time.Time }{date},
-			wantOutput: fmt.Sprintf("Date = %s\n", dateStr),
-		},
-		"datetime field as primitive": {
-			// Using a map here to fail if isStructOrMap() returns true for
-			// time.Time.
-			input: map[string]interface{}{
-				"Date": date,
-				"Int":  1,
-			},
-			wantOutput: fmt.Sprintf("Date = %s\nInt = 1\n", dateStr),
-		},
-		"array fields": {
-			input: struct {
-				IntArray0 [0]int
-				IntArray3 [3]int
-			}{[0]int{}, [3]int{1, 2, 3}},
-			wantOutput: "IntArray0 = []\nIntArray3 = [1, 2, 3]\n",
-		},
-		"slice fields": {
-			input: struct{ IntSliceNil, IntSlice0, IntSlice3 []int }{
-				nil, []int{}, []int{1, 2, 3},
-			},
-			wantOutput: "IntSlice0 = []\nIntSlice3 = [1, 2, 3]\n",
-		},
-		"datetime slices": {
-			input: struct{ DatetimeSlice []time.Time }{
-				[]time.Time{date, date},
-			},
-			wantOutput: fmt.Sprintf("DatetimeSlice = [%s, %s]\n",
-				dateStr, dateStr),
-		},
-		"nested arrays and slices": {
-			input: struct {
-				SliceOfArrays         [][2]int
-				ArrayOfSlices         [2][]int
-				SliceOfArraysOfSlices [][2][]int
-				ArrayOfSlicesOfArrays [2][][2]int
-				SliceOfMixedArrays    [][2]interface{}
-				ArrayOfMixedSlices    [2][]interface{}
-			}{
-				[][2]int{{1, 2}, {3, 4}},
-				[2][]int{{1, 2}, {3, 4}},
-				[][2][]int{
-					{
-						{1, 2}, {3, 4},
-					},
-					{
-						{5, 6}, {7, 8},
-					},
-				},
-				[2][][2]int{
-					{
-						{1, 2}, {3, 4},
-					},
-					{
-						{5, 6}, {7, 8},
-					},
-				},
-				[][2]interface{}{
-					{1, 2}, {"a", "b"},
-				},
-				[2][]interface{}{
-					{1, 2}, {"a", "b"},
-				},
-			},
-			wantOutput: `SliceOfArrays = [[1, 2], [3, 4]]
-ArrayOfSlices = [[1, 2], [3, 4]]
-SliceOfArraysOfSlices = [[[1, 2], [3, 4]], [[5, 6], [7, 8]]]
-ArrayOfSlicesOfArrays = [[[1, 2], [3, 4]], [[5, 6], [7, 8]]]
-SliceOfMixedArrays = [[1, 2], ["a", "b"]]
-ArrayOfMixedSlices = [[1, 2], ["a", "b"]]
-`,
-		},
-		"empty slice": {
-			input:      struct{ Empty []interface{} }{[]interface{}{}},
-			wantOutput: "Empty = []\n",
-		},
-		"(error) slice with element type mismatch (string and integer)": {
-			input:     struct{ Mixed []interface{} }{[]interface{}{1, "a"}},
-			wantError: errArrayMixedElementTypes,
-		},
-		"(error) slice with element type mismatch (integer and float)": {
-			input:     struct{ Mixed []interface{} }{[]interface{}{1, 2.5}},
-			wantError: errArrayMixedElementTypes,
-		},
-		"slice with elems of differing Go types, same TOML types": {
-			input: struct {
-				MixedInts   []interface{}
-				MixedFloats []interface{}
-			}{
-				[]interface{}{
-					int(1), int8(2), int16(3), int32(4), int64(5),
-					uint(1), uint8(2), uint16(3), uint32(4), uint64(5),
-				},
-				[]interface{}{float32(1.5), float64(2.5)},
-			},
-			wantOutput: "MixedInts = [1, 2, 3, 4, 5, 1, 2, 3, 4, 5]\n" +
-				"MixedFloats = [1.5, 2.5]\n",
-		},
-		"(error) slice w/ element type mismatch (one is nested array)": {
-			input: struct{ Mixed []interface{} }{
-				[]interface{}{1, []interface{}{2}},
-			},
-			wantError: errArrayMixedElementTypes,
-		},
-		"(error) slice with 1 nil element": {
-			input:     struct{ NilElement1 []interface{} }{[]interface{}{nil}},
-			wantError: errArrayNilElement,
-		},
-		"(error) slice with 1 nil element (and other non-nil elements)": {
-			input: struct{ NilElement []interface{} }{
-				[]interface{}{1, nil},
-			},
-			wantError: errArrayNilElement,
-		},
-		"simple map": {
-			input:      map[string]int{"a": 1, "b": 2},
-			wantOutput: "a = 1\nb = 2\n",
-		},
-		"map with interface{} value type": {
-			input:      map[string]interface{}{"a": 1, "b": "c"},
-			wantOutput: "a = 1\nb = \"c\"\n",
-		},
-		"map with interface{} value type, some of which are structs": {
-			input: map[string]interface{}{
-				"a": struct{ Int int }{2},
-				"b": 1,
-			},
-			wantOutput: "b = 1\n\n[a]\n  Int = 2\n",
-		},
-		"nested map": {
-			input: map[string]map[string]int{
-				"a": {"b": 1},
-				"c": {"d": 2},
-			},
-			wantOutput: "[a]\n  b = 1\n\n[c]\n  d = 2\n",
-		},
-		"nested struct": {
-			input: struct{ Struct struct{ Int int } }{
-				struct{ Int int }{1},
-			},
-			wantOutput: "[Struct]\n  Int = 1\n",
-		},
-		"nested struct and non-struct field": {
-			input: struct {
-				Struct struct{ Int int }
-				Bool   bool
-			}{struct{ Int int }{1}, true},
-			wantOutput: "Bool = true\n\n[Struct]\n  Int = 1\n",
-		},
-		"2 nested structs": {
-			input: struct{ Struct1, Struct2 struct{ Int int } }{
-				struct{ Int int }{1}, struct{ Int int }{2},
-			},
-			wantOutput: "[Struct1]\n  Int = 1\n\n[Struct2]\n  Int = 2\n",
-		},
-		"deeply nested structs": {
-			input: struct {
-				Struct1, Struct2 struct{ Struct3 *struct{ Int int } }
-			}{
-				struct{ Struct3 *struct{ Int int } }{&struct{ Int int }{1}},
-				struct{ Struct3 *struct{ Int int } }{nil},
-			},
-			wantOutput: "[Struct1]\n  [Struct1.Struct3]\n    Int = 1" +
-				"\n\n[Struct2]\n",
-		},
-		"nested struct with nil struct elem": {
-			input: struct {
-				Struct struct{ Inner *struct{ Int int } }
-			}{
-				struct{ Inner *struct{ Int int } }{nil},
-			},
-			wantOutput: "[Struct]\n",
-		},
-		"nested struct with no fields": {
-			input: struct {
-				Struct struct{ Inner struct{} }
-			}{
-				struct{ Inner struct{} }{struct{}{}},
-			},
-			wantOutput: "[Struct]\n  [Struct.Inner]\n",
-		},
-		"struct with tags": {
-			input: struct {
-				Struct struct {
-					Int int `toml:"_int"`
-				} `toml:"_struct"`
-				Bool bool `toml:"_bool"`
-			}{
-				struct {
-					Int int `toml:"_int"`
-				}{1}, true,
-			},
-			wantOutput: "_bool = true\n\n[_struct]\n  _int = 1\n",
-		},
-		"embedded struct": {
-			input:      struct{ Embedded }{Embedded{1}},
-			wantOutput: "_int = 1\n",
-		},
-		"embedded *struct": {
-			input:      struct{ *Embedded }{&Embedded{1}},
-			wantOutput: "_int = 1\n",
-		},
-		"nested embedded struct": {
-			input: struct {
-				Struct struct{ Embedded } `toml:"_struct"`
-			}{struct{ Embedded }{Embedded{1}}},
-			wantOutput: "[_struct]\n  _int = 1\n",
-		},
-		"nested embedded *struct": {
-			input: struct {
-				Struct struct{ *Embedded } `toml:"_struct"`
-			}{struct{ *Embedded }{&Embedded{1}}},
-			wantOutput: "[_struct]\n  _int = 1\n",
-		},
-		"embedded non-struct": {
-			input:      struct{ NonStruct }{5},
-			wantOutput: "NonStruct = 5\n",
-		},
-		"array of tables": {
-			input: struct {
-				Structs []*struct{ Int int } `toml:"struct"`
-			}{
-				[]*struct{ Int int }{{1}, {3}},
-			},
-			wantOutput: "[[struct]]\n  Int = 1\n\n[[struct]]\n  Int = 3\n",
-		},
-		"array of tables order": {
-			input: map[string]interface{}{
-				"map": map[string]interface{}{
-					"zero": 5,
-					"arr": []map[string]int{
-						{
-							"friend": 5,
-						},
-					},
-				},
-			},
-			wantOutput: "[map]\n  zero = 5\n\n  [[map.arr]]\n    friend = 5\n",
-		},
-		"empty key name": {
-			input:      map[string]int{"": 1},
-			wantOutput: `"" = 1` + "\n",
-		},
-		"key with \\n escape": {
-			input:      map[string]string{"\n": "\n"},
-			wantOutput: `"\n" = "\n"` + "\n",
-		},
-
-		"empty map name": {
-			input: map[string]interface{}{
-				"": map[string]int{"v": 1},
-			},
-			wantOutput: "[\"\"]\n  v = 1\n",
-		},
-		"(error) top-level slice": {
-			input:     []struct{ Int int }{{1}, {2}, {3}},
-			wantError: errNoKey,
-		},
-		"(error) slice of slice": {
-			input: struct {
-				Slices [][]struct{ Int int }
-			}{
-				[][]struct{ Int int }{{{1}}, {{2}}, {{3}}},
-			},
-			wantError: errArrayNoTable,
-		},
-		"(error) map no string key": {
-			input:     map[int]string{1: ""},
-			wantError: errNonString,
-		},
-	}
-	for label, test := range tests {
-		encodeExpected(t, label, test.input, test.wantOutput, test.wantError)
-	}
-}
-
 func TestEncodeNestedTableArrays(t *testing.T) {
 	type song struct {
 		Name string `toml:"name"`
diff --git a/move_test.go b/move_test.go
new file mode 100644
index 0000000..c8ba5a7
--- /dev/null
+++ b/move_test.go
@@ -0,0 +1,398 @@
+package toml
+
+import (
+	"fmt"
+	"testing"
+	"time"
+)
+
+func TestDecodeDatetime(t *testing.T) {
+	tz7 := time.FixedZone("", -3600*7)
+
+	for _, tt := range []struct {
+		in   string
+		want time.Time
+	}{
+		// Offset datetime
+		{"1979-05-27T07:32:00Z", time.Date(1979, 05, 27, 07, 32, 0, 0, time.UTC)},
+		{"1979-05-27T07:32:00.999999Z", time.Date(1979, 05, 27, 07, 32, 0, 999999000, time.UTC)},
+		{"1979-05-27T00:32:00-07:00", time.Date(1979, 05, 27, 00, 32, 0, 0, tz7)},
+		{"1979-05-27T00:32:00.999999-07:00", time.Date(1979, 05, 27, 00, 32, 0, 999999000, tz7)},
+		{"1979-05-27T00:32:00.24-07:00", time.Date(1979, 05, 27, 00, 32, 0, 240000000, tz7)},
+		{"1979-05-27 07:32:00Z", time.Date(1979, 05, 27, 07, 32, 0, 0, time.UTC)},
+		{"1979-05-27t07:32:00z", time.Date(1979, 05, 27, 07, 32, 0, 0, time.UTC)},
+
+		// Local datetime; according to the spec this should be "without any
+		// relation to an offset or timezone. It cannot be converted to an
+		// instant in time without additional information. Conversion to an
+		// instant, if required, is implementation-specific."
+		//
+		// Go doesn't supporting a time without a timezone, so use time.Local.
+		{"1979-05-27T07:32:00", time.Date(1979, 05, 27, 07, 32, 0, 0, time.Local)},
+		{"1979-05-27T07:32:00.999999", time.Date(1979, 05, 27, 07, 32, 0, 999999000, time.Local)},
+		{"1979-05-27T07:32:00.25", time.Date(1979, 05, 27, 07, 32, 0, 250000000, time.Local)},
+
+		{"1979-05-27", time.Date(1979, 05, 27, 0, 0, 0, 0, time.Local)},
+
+		{"07:32:00", time.Date(0, 1, 1, 07, 32, 0, 0, time.Local)},
+		{"07:32:00.999999", time.Date(0, 1, 1, 07, 32, 0, 999999000, time.Local)},
+
+		// Make sure the space between the datetime and "#" isn't lexed.
+		{"1979-05-27T07:32:12-07:00  # c", time.Date(1979, 05, 27, 07, 32, 12, 0, tz7)},
+	} {
+		t.Run(tt.in, func(t *testing.T) {
+			var x struct{ D time.Time }
+			input := "d = " + tt.in
+			if _, err := Decode(input, &x); err != nil {
+				t.Fatalf("got error: %s", err)
+			}
+
+			if h, w := x.D.Format(time.RFC3339Nano), tt.want.Format(time.RFC3339Nano); h != w {
+				t.Errorf("\nhave: %s\nwant: %s", h, w)
+			}
+		})
+	}
+}
+
+func TestEncode(t *testing.T) {
+	type Embedded struct {
+		Int int `toml:"_int"`
+	}
+	type NonStruct int
+
+	date := time.Date(2014, 5, 11, 19, 30, 40, 0, time.UTC)
+	dateStr := "2014-05-11T19:30:40Z"
+
+	tests := map[string]struct {
+		input      interface{}
+		wantOutput string
+		wantError  error
+	}{
+		"bool field": {
+			input: struct {
+				BoolTrue  bool
+				BoolFalse bool
+			}{true, false},
+			wantOutput: "BoolTrue = true\nBoolFalse = false\n",
+		},
+		"int fields": {
+			input: struct {
+				Int   int
+				Int8  int8
+				Int16 int16
+				Int32 int32
+				Int64 int64
+			}{1, 2, 3, 4, 5},
+			wantOutput: "Int = 1\nInt8 = 2\nInt16 = 3\nInt32 = 4\nInt64 = 5\n",
+		},
+		"uint fields": {
+			input: struct {
+				Uint   uint
+				Uint8  uint8
+				Uint16 uint16
+				Uint32 uint32
+				Uint64 uint64
+			}{1, 2, 3, 4, 5},
+			wantOutput: "Uint = 1\nUint8 = 2\nUint16 = 3\nUint32 = 4" +
+				"\nUint64 = 5\n",
+		},
+		"float fields": {
+			input: struct {
+				Float32 float32
+				Float64 float64
+			}{1.5, 2.5},
+			wantOutput: "Float32 = 1.5\nFloat64 = 2.5\n",
+		},
+		"string field": {
+			input:      struct{ String string }{"foo"},
+			wantOutput: "String = \"foo\"\n",
+		},
+		"string field with \\n escape": {
+			input:      struct{ String string }{"foo\n"},
+			wantOutput: "String = \"foo\\n\"\n",
+		},
+		"string field and unexported field": {
+			input: struct {
+				String     string
+				unexported int
+			}{"foo", 0},
+			wantOutput: "String = \"foo\"\n",
+		},
+		"datetime field in UTC": {
+			input:      struct{ Date time.Time }{date},
+			wantOutput: fmt.Sprintf("Date = %s\n", dateStr),
+		},
+		"datetime field as primitive": {
+			// Using a map here to fail if isStructOrMap() returns true for
+			// time.Time.
+			input: map[string]interface{}{
+				"Date": date,
+				"Int":  1,
+			},
+			wantOutput: fmt.Sprintf("Date = %s\nInt = 1\n", dateStr),
+		},
+		"array fields": {
+			input: struct {
+				IntArray0 [0]int
+				IntArray3 [3]int
+			}{[0]int{}, [3]int{1, 2, 3}},
+			wantOutput: "IntArray0 = []\nIntArray3 = [1, 2, 3]\n",
+		},
+		"slice fields": {
+			input: struct{ IntSliceNil, IntSlice0, IntSlice3 []int }{
+				nil, []int{}, []int{1, 2, 3},
+			},
+			wantOutput: "IntSlice0 = []\nIntSlice3 = [1, 2, 3]\n",
+		},
+		"datetime slices": {
+			input: struct{ DatetimeSlice []time.Time }{
+				[]time.Time{date, date},
+			},
+			wantOutput: fmt.Sprintf("DatetimeSlice = [%s, %s]\n",
+				dateStr, dateStr),
+		},
+		"nested arrays and slices": {
+			input: struct {
+				SliceOfArrays         [][2]int
+				ArrayOfSlices         [2][]int
+				SliceOfArraysOfSlices [][2][]int
+				ArrayOfSlicesOfArrays [2][][2]int
+				SliceOfMixedArrays    [][2]interface{}
+				ArrayOfMixedSlices    [2][]interface{}
+			}{
+				[][2]int{{1, 2}, {3, 4}},
+				[2][]int{{1, 2}, {3, 4}},
+				[][2][]int{
+					{
+						{1, 2}, {3, 4},
+					},
+					{
+						{5, 6}, {7, 8},
+					},
+				},
+				[2][][2]int{
+					{
+						{1, 2}, {3, 4},
+					},
+					{
+						{5, 6}, {7, 8},
+					},
+				},
+				[][2]interface{}{
+					{1, 2}, {"a", "b"},
+				},
+				[2][]interface{}{
+					{1, 2}, {"a", "b"},
+				},
+			},
+			wantOutput: `SliceOfArrays = [[1, 2], [3, 4]]
+ArrayOfSlices = [[1, 2], [3, 4]]
+SliceOfArraysOfSlices = [[[1, 2], [3, 4]], [[5, 6], [7, 8]]]
+ArrayOfSlicesOfArrays = [[[1, 2], [3, 4]], [[5, 6], [7, 8]]]
+SliceOfMixedArrays = [[1, 2], ["a", "b"]]
+ArrayOfMixedSlices = [[1, 2], ["a", "b"]]
+`,
+		},
+		"empty slice": {
+			input:      struct{ Empty []interface{} }{[]interface{}{}},
+			wantOutput: "Empty = []\n",
+		},
+		"(error) slice with element type mismatch (string and integer)": {
+			input:     struct{ Mixed []interface{} }{[]interface{}{1, "a"}},
+			wantError: errArrayMixedElementTypes,
+		},
+		"(error) slice with element type mismatch (integer and float)": {
+			input:     struct{ Mixed []interface{} }{[]interface{}{1, 2.5}},
+			wantError: errArrayMixedElementTypes,
+		},
+		"slice with elems of differing Go types, same TOML types": {
+			input: struct {
+				MixedInts   []interface{}
+				MixedFloats []interface{}
+			}{
+				[]interface{}{
+					int(1), int8(2), int16(3), int32(4), int64(5),
+					uint(1), uint8(2), uint16(3), uint32(4), uint64(5),
+				},
+				[]interface{}{float32(1.5), float64(2.5)},
+			},
+			wantOutput: "MixedInts = [1, 2, 3, 4, 5, 1, 2, 3, 4, 5]\n" +
+				"MixedFloats = [1.5, 2.5]\n",
+		},
+		"(error) slice w/ element type mismatch (one is nested array)": {
+			input: struct{ Mixed []interface{} }{
+				[]interface{}{1, []interface{}{2}},
+			},
+			wantError: errArrayMixedElementTypes,
+		},
+		"(error) slice with 1 nil element": {
+			input:     struct{ NilElement1 []interface{} }{[]interface{}{nil}},
+			wantError: errArrayNilElement,
+		},
+		"(error) slice with 1 nil element (and other non-nil elements)": {
+			input: struct{ NilElement []interface{} }{
+				[]interface{}{1, nil},
+			},
+			wantError: errArrayNilElement,
+		},
+		"simple map": {
+			input:      map[string]int{"a": 1, "b": 2},
+			wantOutput: "a = 1\nb = 2\n",
+		},
+		"map with interface{} value type": {
+			input:      map[string]interface{}{"a": 1, "b": "c"},
+			wantOutput: "a = 1\nb = \"c\"\n",
+		},
+		"map with interface{} value type, some of which are structs": {
+			input: map[string]interface{}{
+				"a": struct{ Int int }{2},
+				"b": 1,
+			},
+			wantOutput: "b = 1\n\n[a]\n  Int = 2\n",
+		},
+		"nested map": {
+			input: map[string]map[string]int{
+				"a": {"b": 1},
+				"c": {"d": 2},
+			},
+			wantOutput: "[a]\n  b = 1\n\n[c]\n  d = 2\n",
+		},
+		"nested struct": {
+			input: struct{ Struct struct{ Int int } }{
+				struct{ Int int }{1},
+			},
+			wantOutput: "[Struct]\n  Int = 1\n",
+		},
+		"nested struct and non-struct field": {
+			input: struct {
+				Struct struct{ Int int }
+				Bool   bool
+			}{struct{ Int int }{1}, true},
+			wantOutput: "Bool = true\n\n[Struct]\n  Int = 1\n",
+		},
+		"2 nested structs": {
+			input: struct{ Struct1, Struct2 struct{ Int int } }{
+				struct{ Int int }{1}, struct{ Int int }{2},
+			},
+			wantOutput: "[Struct1]\n  Int = 1\n\n[Struct2]\n  Int = 2\n",
+		},
+		"deeply nested structs": {
+			input: struct {
+				Struct1, Struct2 struct{ Struct3 *struct{ Int int } }
+			}{
+				struct{ Struct3 *struct{ Int int } }{&struct{ Int int }{1}},
+				struct{ Struct3 *struct{ Int int } }{nil},
+			},
+			wantOutput: "[Struct1]\n  [Struct1.Struct3]\n    Int = 1" +
+				"\n\n[Struct2]\n",
+		},
+		"nested struct with nil struct elem": {
+			input: struct {
+				Struct struct{ Inner *struct{ Int int } }
+			}{
+				struct{ Inner *struct{ Int int } }{nil},
+			},
+			wantOutput: "[Struct]\n",
+		},
+		"nested struct with no fields": {
+			input: struct {
+				Struct struct{ Inner struct{} }
+			}{
+				struct{ Inner struct{} }{struct{}{}},
+			},
+			wantOutput: "[Struct]\n  [Struct.Inner]\n",
+		},
+		"struct with tags": {
+			input: struct {
+				Struct struct {
+					Int int `toml:"_int"`
+				} `toml:"_struct"`
+				Bool bool `toml:"_bool"`
+			}{
+				struct {
+					Int int `toml:"_int"`
+				}{1}, true,
+			},
+			wantOutput: "_bool = true\n\n[_struct]\n  _int = 1\n",
+		},
+		"embedded struct": {
+			input:      struct{ Embedded }{Embedded{1}},
+			wantOutput: "_int = 1\n",
+		},
+		"embedded *struct": {
+			input:      struct{ *Embedded }{&Embedded{1}},
+			wantOutput: "_int = 1\n",
+		},
+		"nested embedded struct": {
+			input: struct {
+				Struct struct{ Embedded } `toml:"_struct"`
+			}{struct{ Embedded }{Embedded{1}}},
+			wantOutput: "[_struct]\n  _int = 1\n",
+		},
+		"nested embedded *struct": {
+			input: struct {
+				Struct struct{ *Embedded } `toml:"_struct"`
+			}{struct{ *Embedded }{&Embedded{1}}},
+			wantOutput: "[_struct]\n  _int = 1\n",
+		},
+		"embedded non-struct": {
+			input:      struct{ NonStruct }{5},
+			wantOutput: "NonStruct = 5\n",
+		},
+		"array of tables": {
+			input: struct {
+				Structs []*struct{ Int int } `toml:"struct"`
+			}{
+				[]*struct{ Int int }{{1}, {3}},
+			},
+			wantOutput: "[[struct]]\n  Int = 1\n\n[[struct]]\n  Int = 3\n",
+		},
+		"array of tables order": {
+			input: map[string]interface{}{
+				"map": map[string]interface{}{
+					"zero": 5,
+					"arr": []map[string]int{
+						{
+							"friend": 5,
+						},
+					},
+				},
+			},
+			wantOutput: "[map]\n  zero = 5\n\n  [[map.arr]]\n    friend = 5\n",
+		},
+		"empty key name": {
+			input:      map[string]int{"": 1},
+			wantOutput: `"" = 1` + "\n",
+		},
+		"key with \\n escape": {
+			input:      map[string]string{"\n": "\n"},
+			wantOutput: `"\n" = "\n"` + "\n",
+		},
+
+		"empty map name": {
+			input: map[string]interface{}{
+				"": map[string]int{"v": 1},
+			},
+			wantOutput: "[\"\"]\n  v = 1\n",
+		},
+		"(error) top-level slice": {
+			input:     []struct{ Int int }{{1}, {2}, {3}},
+			wantError: errNoKey,
+		},
+		"(error) slice of slice": {
+			input: struct {
+				Slices [][]struct{ Int int }
+			}{
+				[][]struct{ Int int }{{{1}}, {{2}}, {{3}}},
+			},
+			wantError: errArrayNoTable,
+		},
+		"(error) map no string key": {
+			input:     map[int]string{1: ""},
+			wantError: errNonString,
+		},
+	}
+	for label, test := range tests {
+		encodeExpected(t, label, test.input, test.wantOutput, test.wantError)
+	}
+}
diff --git a/toml_test.go b/toml_test.go
index fb3f4ea..70d80e7 100644
--- a/toml_test.go
+++ b/toml_test.go
@@ -1,26 +1,68 @@
+// +build go1.16
+
 package toml_test
 
 import (
+	"bytes"
+	"encoding/json"
+	"fmt"
+	"log"
+	"math"
+	"path/filepath"
+	"strconv"
+	"strings"
 	"testing"
+	"time"
 
+	"github.com/BurntSushi/toml"
 	tomltest "github.com/BurntSushi/toml-test"
 )
 
+// Test if the error message matches what we want for invalid tests. Every slice
+// entry is tested with strings.Contains.
+//
+// Filepaths are glob'd
+var errorTests = map[string][]string{
+	"encoding-bad-utf8*": {"invalid UTF-8 byte"},
+	"encoding-utf16*":    {"files cannot contain NULL bytes; probably using UTF-16"},
+}
+
+// Test metadata; all keys listed as "keyname: type".
+var metaTests = map[string]string{
+	// TODO: this probably should have albums as a Hash as well?
+	"table-array-implicit": `
+			albums.songs: ArrayHash
+			albums.songs.name: String
+		`,
+}
+
 func TestToml(t *testing.T) {
+	for k := range errorTests { // Make sure patterns are valid.
+		_, err := filepath.Match(k, "")
+		if err != nil {
+			t.Fatal(err)
+		}
+	}
+
 	run := func(t *testing.T, enc bool) {
 		t.Helper()
 		r := tomltest.Runner{
-			Files:     tomltest.EmbeddedTests(),
-			Encoder:   enc,
-			ParserCmd: []string{"toml-test-decoder"},
-			SkipTests: []string{"valid/key-dotted",
+			Files:   tomltest.EmbeddedTests(),
+			Encoder: enc,
+			Parser:  parser{},
+			SkipTests: []string{
+				"valid/key-dotted",
 				"valid/datetime-local-date",
 				"valid/datetime-local-time",
-				"valid/datetime-local"},
-		}
-		if enc {
-			r.ParserCmd = []string{"toml-test-encoder"}
-			r.SkipTests = append(r.SkipTests, "valid/inline-table-nest")
+				"valid/datetime-local",
+				"valid/array-mix-string-table",
+				"valid/inline-table-nest",
+
+				"invalid/control-comment*",
+				"invalid/inline-table-trailing-comma",
+				"invalid/string-literal-multiline-quotes",
+				"invalid/string-multiline-escape-space",
+			},
 		}
 
 		tests, err := r.Run()
@@ -29,12 +71,23 @@ func TestToml(t *testing.T) {
 		}
 
 		for _, test := range tests.Tests {
-			if test.Failed() {
-				t.Run(test.Path, func(t *testing.T) {
-					t.Errorf("\nError:\n%s\n\nInput:\n%s\nOutput:\n%s\nWant:\n%s\n",
+			t.Run(test.Path, func(t *testing.T) {
+				if test.Failed() {
+					t.Fatalf("\nError:\n%s\n\nInput:\n%s\nOutput:\n%s\nWant:\n%s\n",
 						test.Failure, test.Input, test.Output, test.Want)
-				})
-			}
+					return
+				}
+
+				// Test metadata
+				if !enc && test.Type() == tomltest.TypeValid {
+					testMeta(t, test)
+				}
+
+				// Test error message.
+				if test.Type() == tomltest.TypeInvalid {
+					testError(t, test)
+				}
+			})
 		}
 		t.Logf("passed: %d; failed: %d; skipped: %d", tests.Passed, tests.Failed, tests.Skipped)
 	}
@@ -42,3 +95,222 @@ func TestToml(t *testing.T) {
 	t.Run("decode", func(t *testing.T) { run(t, false) })
 	t.Run("encode", func(t *testing.T) { run(t, true) })
 }
+
+func testMeta(t *testing.T, test tomltest.Test) {
+	want, ok := metaTests[filepath.Base(test.Path)]
+	if !ok {
+		return
+	}
+	var s interface{}
+	meta, err := toml.Decode(test.Input, &s)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	var b strings.Builder
+	for _, k := range meta.Keys() {
+		ks := k.String()
+		b.WriteString(ks)
+		b.WriteString(": ")
+		b.WriteString(meta.Type(ks))
+		b.WriteByte('\n')
+	}
+	have := b.String()
+	have = have[:len(have)-1] // Trailing \n
+
+	want = strings.ReplaceAll(strings.TrimSpace(want), "\t", "")
+	if have != want {
+		t.Errorf("MetaData wrong\nhave:\n%s\nwant:\n%s", have, want)
+	}
+}
+
+func testError(t *testing.T, test tomltest.Test) {
+	path := strings.TrimPrefix(test.Path, "invalid/")
+
+	errs, ok := errorTests[path]
+	if !ok {
+		for k := range errorTests {
+			ok, _ = filepath.Match(k, path)
+			if ok {
+				errs = errorTests[k]
+				break
+			}
+		}
+	}
+	if !ok {
+		return
+	}
+
+	for _, e := range errs {
+		if !strings.Contains(test.Output, e) {
+			t.Errorf("\nwrong error message\nhave: %s\nwant: %s", test.Output, e)
+		}
+	}
+}
+
+type parser struct{}
+
+func (p parser) Encode(input string) (output string, outputIsError bool, retErr error) {
+	defer func() {
+		if r := recover(); r != nil {
+			switch rr := r.(type) {
+			case error:
+				retErr = rr
+			default:
+				retErr = fmt.Errorf("%s", rr)
+			}
+		}
+	}()
+
+	var tmp interface{}
+	err := json.Unmarshal([]byte(input), &tmp)
+	if err != nil {
+		return "", false, err
+	}
+
+	buf := new(bytes.Buffer)
+	err = toml.NewEncoder(buf).Encode(p.removeJSONTags(tmp))
+	if err != nil {
+		return err.Error(), true, retErr
+	}
+
+	return buf.String(), false, retErr
+}
+
+func (p parser) Decode(input string) (output string, outputIsError bool, retErr error) {
+	defer func() {
+		if r := recover(); r != nil {
+			switch rr := r.(type) {
+			case error:
+				retErr = rr
+			default:
+				retErr = fmt.Errorf("%s", rr)
+			}
+		}
+	}()
+
+	var d interface{}
+	if _, err := toml.Decode(input, &d); err != nil {
+		return err.Error(), true, retErr
+	}
+
+	j, err := json.MarshalIndent(p.addJSONTags(d), "", "  ")
+	if err != nil {
+		return "", false, err
+	}
+	return string(j), false, retErr
+}
+
+func (p parser) addJSONTags(tomlData interface{}) interface{} {
+	tag := func(typeName string, data interface{}) map[string]interface{} {
+		return map[string]interface{}{
+			"type":  typeName,
+			"value": data,
+		}
+	}
+
+	switch orig := tomlData.(type) {
+	default:
+		panic(fmt.Sprintf("Unknown type: %T", tomlData))
+
+	case map[string]interface{}:
+		typed := make(map[string]interface{}, len(orig))
+		for k, v := range orig {
+			typed[k] = p.addJSONTags(v)
+		}
+		return typed
+	case []map[string]interface{}:
+		typed := make([]map[string]interface{}, len(orig))
+		for i, v := range orig {
+			typed[i] = p.addJSONTags(v).(map[string]interface{})
+		}
+		return typed
+	case []interface{}:
+		typed := make([]interface{}, len(orig))
+		for i, v := range orig {
+			typed[i] = p.addJSONTags(v)
+		}
+		return typed
+	case time.Time:
+		return tag("datetime", orig.Format("2006-01-02T15:04:05.999999999Z07:00"))
+	case bool:
+		return tag("bool", fmt.Sprintf("%v", orig))
+	case int64:
+		return tag("integer", fmt.Sprintf("%d", orig))
+	case float64:
+		if math.IsNaN(orig) {
+			return tag("float", "nan")
+		}
+		return tag("float", fmt.Sprintf("%v", orig))
+	case string:
+		return tag("string", orig)
+	}
+}
+
+func (p parser) removeJSONTags(typedJson interface{}) interface{} {
+	in := func(key string, m map[string]interface{}) bool {
+		_, ok := m[key]
+		return ok
+	}
+
+	untag := func(typed map[string]interface{}) interface{} {
+		t := typed["type"].(string)
+		v := typed["value"]
+		switch t {
+		case "string":
+			return v.(string)
+		case "integer":
+			v := v.(string)
+			n, err := strconv.Atoi(v)
+			if err != nil {
+				log.Fatalf("Could not parse '%s' as integer: %s", v, err)
+			}
+			return n
+		case "float":
+			v := v.(string)
+			f, err := strconv.ParseFloat(v, 64)
+			if err != nil {
+				log.Fatalf("Could not parse '%s' as float64: %s", v, err)
+			}
+			return f
+		case "datetime":
+			v := v.(string)
+			t, err := time.Parse("2006-01-02T15:04:05.999999999Z07:00", v)
+			if err != nil {
+				log.Fatalf("Could not parse '%s' as a datetime: %s", v, err)
+			}
+			return t
+		case "bool":
+			v := v.(string)
+			switch v {
+			case "true":
+				return true
+			case "false":
+				return false
+			}
+			log.Fatalf("Could not parse '%s' as a boolean.", v)
+		}
+		log.Fatalf("Unrecognized tag type '%s'.", t)
+		panic("unreachable")
+	}
+
+	switch v := typedJson.(type) {
+	case map[string]interface{}:
+		if len(v) == 2 && in("type", v) && in("value", v) {
+			return untag(v)
+		}
+		m := make(map[string]interface{}, len(v))
+		for k, v2 := range v {
+			m[k] = p.removeJSONTags(v2)
+		}
+		return m
+	case []interface{}:
+		a := make([]interface{}, len(v))
+		for i := range v {
+			a[i] = p.removeJSONTags(v[i])
+		}
+		return a
+	}
+	log.Fatalf("Unrecognized JSON format '%T'.", typedJson)
+	panic("unreachable")
+}