Repository: TomWright/dasel
Pull request #289 (2023-01-20 18:21:21)

Ordered maps
This is my first attempt at supporting ordered maps, starting with JSON.

It's become apparent there's no way to achieve this functionality unless I write my own encoders + decoders for all supported file formats.

This is a very large undertaking, and given the fact that ordered JSON isn't even a part of the JSON spec itself I'm not sure it's worth doing due to the amount of complexity it adds to the project.

Of course this is only regarding JSON as the majority of my tests are placed around it and it gave me an easy way to check a variety of use-cases quickly and easily, but may well transfer to other data formats.


Patch:
diff --git a/dencoding/README.md b/dencoding/README.md
new file mode 100644
index 0000000..83ff234
--- /dev/null
+++ b/dencoding/README.md
@@ -0,0 +1,15 @@
+# dencoding - Dasel Encoding
+
+This package provides encoding implementations for all supported file formats.
+
+The main difference is that it aims to keep maps ordered, where the default encoders/decoders do not.
+
+## Support formats
+
+### Decoding
+
+Custom decoders are required to ensure that map/object values are decoded into the `Map` type rather than a standard `map[string]any`.
+
+### Encoding
+
+The `Map` type must have the appropriate Marshal func on it to ensure marshalling it in the desired format retains the ordering.
diff --git a/dencoding/json.go b/dencoding/json.go
new file mode 100644
index 0000000..5a82c3d
--- /dev/null
+++ b/dencoding/json.go
@@ -0,0 +1,20 @@
+package dencoding
+
+import "encoding/json"
+
+const (
+	jsonOpenObject  = json.Delim('{')
+	jsonCloseObject = json.Delim('}')
+	jsonOpenArray   = json.Delim('[')
+	jsonCloseArray  = json.Delim(']')
+)
+
+// JSONEncoderOption is identifies an option that can be applied to a JSON encoder.
+type JSONEncoderOption interface {
+	ApplyEncoder(encoder *JSONEncoder)
+}
+
+// JSONDecoderOption is identifies an option that can be applied to a JSON decoder.
+type JSONDecoderOption interface {
+	ApplyDecoder(decoder *JSONDecoder)
+}
diff --git a/dencoding/json_decoder.go b/dencoding/json_decoder.go
new file mode 100644
index 0000000..26b5f78
--- /dev/null
+++ b/dencoding/json_decoder.go
@@ -0,0 +1,174 @@
+package dencoding
+
+import (
+	"encoding/json"
+	"fmt"
+	"io"
+	"reflect"
+	"strings"
+)
+
+// JSONDecoder wraps a standard json encoder to implement custom ordering logic.
+type JSONDecoder struct {
+	decoder *json.Decoder
+}
+
+// NewJSONDecoder returns a new dencoding JSONDecoder.
+func NewJSONDecoder(r io.Reader, options ...JSONDecoderOption) *JSONDecoder {
+	jsonDecoder := json.NewDecoder(r)
+	jsonDecoder.UseNumber()
+	decoder := &JSONDecoder{
+		decoder: jsonDecoder,
+	}
+	for _, o := range options {
+		o.ApplyDecoder(decoder)
+	}
+	return decoder
+}
+
+// Decode decodes the next item found in the decoder and writes it to v.
+func (decoder *JSONDecoder) Decode(v any) error {
+	rv := reflect.ValueOf(v)
+	if rv.Kind() != reflect.Pointer || rv.IsNil() {
+		return fmt.Errorf("invalid decode target: %s", reflect.TypeOf(v))
+	}
+
+	rve := rv.Elem()
+
+	t, err := decoder.decoder.Token()
+	if err != nil {
+		return err
+	}
+
+	switch t {
+	case jsonOpenObject:
+		object, err := decoder.decodeObject()
+		if err != nil {
+			return fmt.Errorf("could not decode object: %w", err)
+		}
+		rve.Set(reflect.ValueOf(object))
+	case jsonOpenArray:
+		arr, err := decoder.decodeArray()
+		if err != nil {
+			return fmt.Errorf("could not decode array: %w", err)
+		}
+		rve.Set(reflect.ValueOf(arr))
+	default:
+		value, err := decoder.decodeValue(t)
+		if err != nil {
+			return err
+		}
+		rve.Set(reflect.ValueOf(value))
+	}
+
+	return nil
+}
+
+func (decoder *JSONDecoder) decodeObject() (*Map, error) {
+	res := NewMap()
+
+	var key any = nil
+
+	for {
+		t, err := decoder.decoder.Token()
+		if err != nil {
+			// We don't expect an EOF here since we're in the middle of processing an object.
+			return res, err
+		}
+
+		switch t {
+		case jsonOpenArray:
+			if key == nil {
+				return res, fmt.Errorf("unexpected token: %v", t)
+			}
+			value, err := decoder.decodeArray()
+			if err != nil {
+				return res, err
+			}
+			res.Set(key.(string), value)
+			key = nil
+		case jsonCloseArray:
+			return res, fmt.Errorf("unexpected token: %v", t)
+		case jsonCloseObject:
+			return res, nil
+		case jsonOpenObject:
+			if key == nil {
+				return res, fmt.Errorf("unexpected token: %v", t)
+			}
+			value, err := decoder.decodeObject()
+			if err != nil {
+				return res, err
+			}
+			res.Set(key.(string), value)
+			key = nil
+		default:
+			if key == nil {
+				key = t
+			} else {
+				value, err := decoder.decodeValue(t)
+				if err != nil {
+					return nil, err
+				}
+				res.Set(key.(string), value)
+				key = nil
+			}
+		}
+	}
+}
+
+func (decoder *JSONDecoder) decodeValue(t json.Token) (any, error) {
+	switch tv := t.(type) {
+	case json.Number:
+		strNum := tv.String()
+		if strings.Contains(strNum, ".") {
+			floatNum, err := tv.Float64()
+			if err == nil {
+				return floatNum, nil
+			}
+			return nil, err
+		}
+		intNum, err := tv.Int64()
+		if err == nil {
+			return intNum, nil
+		}
+
+		return nil, err
+	}
+	return t, nil
+}
+
+func (decoder *JSONDecoder) decodeArray() ([]any, error) {
+	res := make([]any, 0)
+	for {
+		t, err := decoder.decoder.Token()
+		if err != nil {
+			// We don't expect an EOF here since we're in the middle of processing an object.
+			return res, err
+		}
+
+		switch t {
+		case jsonOpenArray:
+			value, err := decoder.decodeArray()
+			if err != nil {
+				return res, err
+			}
+			res = append(res, value)
+		case jsonCloseArray:
+			return res, nil
+		case jsonCloseObject:
+			return res, fmt.Errorf("unexpected token: %t", t)
+		case jsonOpenObject:
+			value, err := decoder.decodeObject()
+			if err != nil {
+				return res, err
+			}
+			res = append(res, value)
+		default:
+			value, err := decoder.decodeValue(t)
+			if err != nil {
+				return nil, err
+			}
+			res = append(res, value)
+		}
+	}
+}
diff --git a/dencoding/json_encoder.go b/dencoding/json_encoder.go
new file mode 100644
index 0000000..ded7a07
--- /dev/null
+++ b/dencoding/json_encoder.go
@@ -0,0 +1,88 @@
+package dencoding
+
+import (
+	"bytes"
+	"encoding/json"
+	"io"
+)
+
+// JSONEncoder wraps a standard json encoder to implement custom ordering logic.
+type JSONEncoder struct {
+	encoder *json.Encoder
+}
+
+// NewJSONEncoder returns a new dencoding JSONEncoder.
+func NewJSONEncoder(w io.Writer, options ...JSONEncoderOption) *JSONEncoder {
+	jsonEncoder := json.NewEncoder(w)
+	encoder := &JSONEncoder{
+		encoder: jsonEncoder,
+	}
+	for _, o := range options {
+		o.ApplyEncoder(encoder)
+	}
+	return encoder
+}
+
+// Encode encodes the given value and writes the encodes bytes to the stream.
+func (encoder *JSONEncoder) Encode(v any) error {
+	// We rely on Map.MarshalJSON to ensure ordering.
+	return encoder.encoder.Encode(v)
+}
+
+// Close cleans up the encoder.
+func (encoder *JSONEncoder) Close() error {
+	return nil
+}
+
+// JSONEscapeHTML enables or disables html escaping when encoding JSON.
+func JSONEscapeHTML(escape bool) JSONEncoderOption {
+	return jsonEncodeHTMLOption{escapeHTML: escape}
+}
+
+type jsonEncodeHTMLOption struct {
+	escapeHTML bool
+}
+
+func (option jsonEncodeHTMLOption) ApplyEncoder(encoder *JSONEncoder) {
+	encoder.encoder.SetEscapeHTML(option.escapeHTML)
+}
+
+// JSONEncodeIndent sets the indentation when encoding JSON.
+func JSONEncodeIndent(prefix string, indent string) JSONEncoderOption {
+	return jsonEncodeIndent{prefix: prefix, indent: indent}
+}
+
+type jsonEncodeIndent struct {
+	prefix string
+	indent string
+}
+
+func (option jsonEncodeIndent) ApplyEncoder(encoder *JSONEncoder) {
+	encoder.encoder.SetIndent(option.prefix, option.indent)
+}
+
+// MarshalJSON JSON encodes the map and returns the bytes.
+// This maintains ordering.
+func (m *Map) MarshalJSON() ([]byte, error) {
+	buf := new(bytes.Buffer)
+	buf.Write([]byte(`{`))
+	for i, key := range m.keys {
+		last := i == len(m.keys)-1
+		keyBytes, err := json.Marshal(key)
+		if err != nil {
+			return nil, err
+		}
+		buf.Write(keyBytes)
+		buf.Write([]byte(`:`))
+		valBytes, err := json.Marshal(m.data[key])
+		if err != nil {
+			return nil, err
+		}
+		buf.Write(valBytes)
+		if !last {
+			buf.Write([]byte(`,`))
+		}
+	}
+	buf.Write([]byte(`}`))
+	return buf.Bytes(), nil
+}
diff --git a/dencoding/keyvalue.go b/dencoding/keyvalue.go
new file mode 100644
index 0000000..e3e4f28
--- /dev/null
+++ b/dencoding/keyvalue.go
@@ -0,0 +1,7 @@
+package dencoding
+
+// KeyValue is a single key value pair from a *Map.
+type KeyValue struct {
+	Key   string
+	Value any
+}
diff --git a/dencoding/map.go b/dencoding/map.go
new file mode 100644
index 0000000..69aa8fd
--- /dev/null
+++ b/dencoding/map.go
@@ -0,0 +1,90 @@
+package dencoding
+
+// NewMap returns a new *Map that has its values initialised.
+func NewMap() *Map {
+	keys := make([]string, 0)
+	data := make(map[string]any)
+	return &Map{
+		keys: keys,
+		data: data,
+	}
+}
+
+// FromMap creates a *Map from the input map.
+// Note that while the contents will be ordered, the ordering is not
+// guaranteed since the input map is unordered.
+func FromMap(source map[string]any) *Map {
+	m := NewMap()
+	for k, v := range source {
+		m.Set(k, v)
+	}
+	return m
+}
+
+// Map is a map implementation that maintains ordering of keys.
+type Map struct {
+	// keys contains the keys within the map in the order they were added.
+	keys []string
+	// data contains the actual map data.
+	data map[string]any
+}
+
+// Get returns the value found under the given key.
+func (m *Map) Get(key string) (any, bool) {
+	v, ok := m.data[key]
+	return v, ok
+}
+
+// Set sets a value under the given key.
+func (m *Map) Set(key string, value any) *Map {
+	if _, ok := m.data[key]; ok {
+		m.data[key] = value
+	} else {
+		m.keys = append(m.keys, key)
+		m.data[key] = value
+	}
+	return m
+}
+
+// Delete deletes the value found under the given key.
+func (m *Map) Delete(key string) *Map {
+	// Delete the data entry.
+	delete(m.data, key)
+
+	// Remove the item from the keys.
+	foundIndex := -1
+	for i, k := range m.keys {
+		if k == key {
+			foundIndex = i
+			break
+		}
+	}
+
+	if foundIndex >= 0 {
+		m.keys = append((m.keys)[:foundIndex], (m.keys)[foundIndex+1:]...)
+	}
+
+	return m
+}
+
+// KeyValues returns the KeyValue pairs within the map, in the order that they were added.
+func (m *Map) KeyValues() []KeyValue {
+	res := make([]KeyValue, 0)
+	for _, key := range m.keys {
+		res = append(res, KeyValue{
+			Key:   key,
+			Value: m.data[key],
+		})
+	}
+	return res
+}
+
+// Keys returns all the keys from the map.
+func (m *Map) Keys() []string {
+	return m.keys
+}
+
+// UnorderedData returns all the data from the map.
+func (m *Map) UnorderedData() map[string]any {
+	return m.data
+}
diff --git a/dencoding/toml.go b/dencoding/toml.go
new file mode 100644
index 0000000..8cc6478
--- /dev/null
+++ b/dencoding/toml.go
@@ -0,0 +1,11 @@
+package dencoding
+
+// TOMLEncoderOption is identifies an option that can be applied to a TOML encoder.
+type TOMLEncoderOption interface {
+	ApplyEncoder(encoder *TOMLEncoder)
+}
+
+// TOMLDecoderOption is identifies an option that can be applied to a TOML decoder.
+type TOMLDecoderOption interface {
+	ApplyDecoder(decoder *TOMLDecoder)
+}
diff --git a/dencoding/toml_decoder.go b/dencoding/toml_decoder.go
new file mode 100644
index 0000000..8066088
--- /dev/null
+++ b/dencoding/toml_decoder.go
@@ -0,0 +1,36 @@
+package dencoding
+
+import (
+	"github.com/pelletier/go-toml/v2"
+	"github.com/pelletier/go-toml/v2/unstable"
+	"io"
+)
+
+// TOMLDecoder wraps a standard toml encoder to implement custom ordering logic.
+type TOMLDecoder struct {
+	reader io.Reader
+	p      *unstable.Parser
+}
+
+// NewTOMLDecoder returns a new dencoding TOMLDecoder.
+func NewTOMLDecoder(r io.Reader, options ...TOMLDecoderOption) *TOMLDecoder {
+	decoder := &TOMLDecoder{
+		reader: r,
+	}
+	for _, o := range options {
+		o.ApplyDecoder(decoder)
+	}
+	return decoder
+}
+
+// Decode decodes the next item found in the decoder and writes it to v.
+func (decoder *TOMLDecoder) Decode(v any) error {
+	data, err := io.ReadAll(decoder.reader)
+	if err != nil {
+		return err
+	}
+	if len(data) == 0 {
+		return io.EOF
+	}
+	return toml.Unmarshal(data, v)
+}
diff --git a/dencoding/toml_encoder.go b/dencoding/toml_encoder.go
new file mode 100644
index 0000000..998d7b1
--- /dev/null
+++ b/dencoding/toml_encoder.go
@@ -0,0 +1,99 @@
+package dencoding
+
+import (
+	"bytes"
+	"github.com/pelletier/go-toml/v2"
+	"io"
+)
+
+// TOMLEncoder wraps a standard toml encoder to implement custom ordering logic.
+type TOMLEncoder struct {
+	encoder *toml.Encoder
+	writer  io.Writer
+	buffer  *bytes.Buffer
+}
+
+// NewTOMLEncoder returns a new dencoding TOMLEncoder.
+func NewTOMLEncoder(w io.Writer, options ...TOMLEncoderOption) *TOMLEncoder {
+	buffer := new(bytes.Buffer)
+	tomlEncoder := toml.NewEncoder(buffer)
+	tomlEncoder.SetIndentTables(false)
+	encoder := &TOMLEncoder{
+		writer:  w,
+		encoder: tomlEncoder,
+		buffer:  buffer,
+	}
+	for _, o := range options {
+		o.ApplyEncoder(encoder)
+	}
+	return encoder
+}
+
+// Encode encodes the given value and writes the encodes bytes to the stream.
+func (encoder *TOMLEncoder) Encode(v any) error {
+	// No ordering is done here.
+	adjusted := removeDencodingMap(v)
+	if err := encoder.encoder.Encode(adjusted); err != nil {
+		return err
+	}
+	data, err := io.ReadAll(encoder.buffer)
+	if err != nil {
+		return err
+	}
+	if _, err := encoder.writer.Write(data); err != nil {
+		return err
+	}
+	newline := []byte("\n")
+	if !bytes.HasSuffix(data, newline) {
+		if _, err := encoder.writer.Write(newline); err != nil {
+			return err
+		}
+	}
+	return nil
+}
+
+// Close cleans up the encoder.
+func (encoder *TOMLEncoder) Close() error {
+	return nil
+}
+
+func removeDencodingMap(value any) any {
+	switch v := value.(type) {
+	case []any:
+		return removeDencodingMapFromArray(v)
+	case map[string]any:
+		return removeDencodingMapFromMap(v)
+	case *Map:
+		return removeDencodingMap(v.data)
+	default:
+		return v
+	}
+}
+
+func removeDencodingMapFromArray(value []any) []any {
+	for k, v := range value {
+		value[k] = removeDencodingMap(v)
+	}
+	return value
+}
+
+func removeDencodingMapFromMap(value map[string]any) map[string]any {
+	for k, v := range value {
+		value[k] = removeDencodingMap(v)
+	}
+	return value
+}
+
+// TOMLIndentSymbol sets the indentation when encoding TOML.
+func TOMLIndentSymbol(symbol string) TOMLEncoderOption {
+	return tomlEncodeSymbol{symbol: symbol}
+}
+
+type tomlEncodeSymbol struct {
+	symbol string
+}
+
+func (option tomlEncodeSymbol) ApplyEncoder(encoder *TOMLEncoder) {
+	encoder.encoder.SetIndentSymbol(option.symbol)
+	encoder.encoder.SetIndentTables(option.symbol != "")
+}
diff --git a/dencoding/yaml.go b/dencoding/yaml.go
new file mode 100644
index 0000000..7c36a4c
--- /dev/null
+++ b/dencoding/yaml.go
@@ -0,0 +1,22 @@
+package dencoding
+
+const (
+	yamlTagString = "!!str"
+	yamlTagMap    = "!!map"
+	yamlTagArray  = "!!seq"
+	yamlTagNull   = "!!null"
+	yamlTagBinary = "!!binary"
+	yamlTagBool   = "!!bool"
+	yamlTagInt    = "!!int"
+	yamlTagFloat  = "!!float"
+)
+
+// YAMLEncoderOption is identifies an option that can be applied to a YAML encoder.
+type YAMLEncoderOption interface {
+	ApplyEncoder(encoder *YAMLEncoder)
+}
+
+// YAMLDecoderOption is identifies an option that can be applied to a YAML decoder.
+type YAMLDecoderOption interface {
+	ApplyDecoder(decoder *YAMLDecoder)
+}
diff --git a/dencoding/yaml_decoder.go b/dencoding/yaml_decoder.go
new file mode 100644
index 0000000..87609f2
--- /dev/null
+++ b/dencoding/yaml_decoder.go
@@ -0,0 +1,130 @@
+package dencoding
+
+import (
+	"fmt"
+	"github.com/tomwright/dasel/v2/util"
+	"gopkg.in/yaml.v3"
+	"io"
+	"reflect"
+	"strconv"
+)
+
+// YAMLDecoder wraps a standard yaml encoder to implement custom ordering logic.
+type YAMLDecoder struct {
+	decoder *yaml.Decoder
+}
+
+// NewYAMLDecoder returns a new dencoding YAMLDecoder.
+func NewYAMLDecoder(r io.Reader, options ...YAMLDecoderOption) *YAMLDecoder {
+	yamlDecoder := yaml.NewDecoder(r)
+	decoder := &YAMLDecoder{
+		decoder: yamlDecoder,
+	}
+	for _, o := range options {
+		o.ApplyDecoder(decoder)
+	}
+	return decoder
+}
+
+// Decode decodes the next item found in the decoder and writes it to v.
+func (decoder *YAMLDecoder) Decode(v any) error {
+	rv := reflect.ValueOf(v)
+	if rv.Kind() != reflect.Pointer || rv.IsNil() {
+		return fmt.Errorf("invalid decode target: %s", reflect.TypeOf(v))
+	}
+
+	rve := rv.Elem()
+
+	node, err := decoder.nextNode()
+	if err != nil {
+		return err
+	}
+
+	if node.Kind == yaml.DocumentNode && len(node.Content) == 1 && node.Content[0].ShortTag() == yamlTagNull {
+		return io.EOF
+	}
+
+	val, err := decoder.getNodeValue(node)
+	if err != nil {
+		return err
+	}
+
+	rve.Set(reflect.ValueOf(val))
+	return nil
+}
+
+func (decoder *YAMLDecoder) getNodeValue(node *yaml.Node) (any, error) {
+	switch node.Kind {
+	case yaml.DocumentNode:
+		return decoder.getNodeValue(node.Content[0])
+	case yaml.MappingNode:
+		return decoder.getMappingNodeValue(node)
+	case yaml.SequenceNode:
+		return decoder.getSequenceNodeValue(node)
+	case yaml.ScalarNode:
+		return decoder.getScalarNodeValue(node)
+	default:
+		return nil, fmt.Errorf("unhandled node kind: %v", node.Kind)
+	}
+}
+
+func (decoder *YAMLDecoder) getMappingNodeValue(node *yaml.Node) (any, error) {
+	res := NewMap()
+
+	for i := 0; i < len(node.Content); i += 2 {
+		keyNode := node.Content[i]
+		valueNode := node.Content[i+1]
+
+		keyValue, err := decoder.getNodeValue(keyNode)
+		if err != nil {
+			return nil, err
+		}
+		value, err := decoder.getNodeValue(valueNode)
+		if err != nil {
+			return nil, err
+		}
+
+		key := util.ToString(keyValue)
+
+		res.Set(key, value)
+	}
+
+	return res, nil
+}
+
+func (decoder *YAMLDecoder) getSequenceNodeValue(node *yaml.Node) (any, error) {
+	res := make([]any, len(node.Content))
+	for k, n := range node.Content {
+		val, err := decoder.getNodeValue(n)
+		if err != nil {
+			return nil, err
+		}
+		res[k] = val
+	}
+	return res, nil
+}
+
+func (decoder *YAMLDecoder) getScalarNodeValue(node *yaml.Node) (any, error) {
+	switch node.ShortTag() {
+	case yamlTagNull:
+		return nil, nil
+	case yamlTagBool:
+		return node.Value == "true", nil
+	case yamlTagFloat:
+		return strconv.ParseFloat(node.Value, 64)
+	case yamlTagInt:
+		return strconv.ParseInt(node.Value, 10, 64)
+	case yamlTagString:
+		return node.Value, nil
+	default:
+		return nil, fmt.Errorf("unhandled scalar node tag: %v", node.ShortTag())
+	}
+}
+
+func (decoder *YAMLDecoder) nextNode() (*yaml.Node, error) {
+	var node yaml.Node
+	if err := decoder.decoder.Decode(&node); err != nil {
+		return nil, err
+	}
+	return &node, nil
+}
diff --git a/dencoding/yaml_encoder.go b/dencoding/yaml_encoder.go
new file mode 100644
index 0000000..586521d
--- /dev/null
+++ b/dencoding/yaml_encoder.go
@@ -0,0 +1,111 @@
+package dencoding
+
+import (
+	"github.com/tomwright/dasel/v2/util"
+	"gopkg.in/yaml.v3"
+	"io"
+)
+
+// YAMLEncoder wraps a standard yaml encoder to implement custom ordering logic.
+type YAMLEncoder struct {
+	encoder *yaml.Encoder
+}
+
+// NewYAMLEncoder returns a new dencoding YAMLEncoder.
+func NewYAMLEncoder(w io.Writer, options ...YAMLEncoderOption) *YAMLEncoder {
+	yamlEncoder := yaml.NewEncoder(w)
+	encoder := &YAMLEncoder{
+		encoder: yamlEncoder,
+	}
+	for _, o := range options {
+		o.ApplyEncoder(encoder)
+	}
+	return encoder
+}
+
+// Encode encodes the given value and writes the encodes bytes to the stream.
+func (encoder *YAMLEncoder) Encode(v any) error {
+	// We rely on Map.MarshalYAML to ensure ordering.
+	return encoder.encoder.Encode(v)
+}
+
+// Close cleans up the encoder.
+func (encoder *YAMLEncoder) Close() error {
+	return encoder.encoder.Close()
+}
+
+// MarshalYAML YAML encodes the map and returns the bytes.
+// This maintains ordering.
+func (m *Map) MarshalYAML() (any, error) {
+	return yamlOrderedMapToNode(m)
+}
+
+// YAMLEncodeIndent sets the indentation when encoding YAML.
+func YAMLEncodeIndent(spaces int) YAMLEncoderOption {
+	return yamlEncodeIndent{spaces: spaces}
+}
+
+type yamlEncodeIndent struct {
+	spaces int
+}
+
+func (option yamlEncodeIndent) ApplyEncoder(encoder *YAMLEncoder) {
+	encoder.encoder.SetIndent(option.spaces)
+}
+
+func yamlValueToNode(value any) (*yaml.Node, error) {
+	switch v := value.(type) {
+	case *Map:
+		return yamlOrderedMapToNode(v)
+	case []any:
+		return yamlSliceToNode(v)
+	default:
+		return yamlScalarToNode(v)
+	}
+}
+
+func yamlOrderedMapToNode(value *Map) (*yaml.Node, error) {
+	mapNode := &yaml.Node{
+		Kind:    yaml.MappingNode,
+		Style:   yaml.TaggedStyle & yaml.DoubleQuotedStyle & yaml.SingleQuotedStyle & yaml.LiteralStyle & yaml.FoldedStyle & yaml.FlowStyle,
+		Content: make([]*yaml.Node, 0),
+	}
+
+	for _, key := range value.keys {
+		keyNode, err := yamlValueToNode(key)
+		if err != nil {
+			return nil, err
+		}
+		valueNode, err := yamlValueToNode(value.data[key])
+		if err != nil {
+			return nil, err
+		}
+		mapNode.Content = append(mapNode.Content, keyNode, valueNode)
+	}
+
+	return mapNode, nil
+}
+
+func yamlSliceToNode(value []any) (*yaml.Node, error) {
+	node := &yaml.Node{
+		Kind:    yaml.SequenceNode,
+		Content: make([]*yaml.Node, len(value)),
+	}
+
+	for i, v := range value {
+		indexNode, err := yamlValueToNode(v)
+		if err != nil {
+			return nil, err
+		}
+		node.Content[i] = indexNode
+	}
+
+	return node, nil
+}
+
+func yamlScalarToNode(value any) (*yaml.Node, error) {
+	return &yaml.Node{
+		Kind:  yaml.ScalarNode,
+		Value: util.ToString(value),
+	}, nil
+}
diff --git a/func_all.go b/func_all.go
index 8ae7a07..c22a408 100644
--- a/func_all.go
+++ b/func_all.go
@@ -2,6 +2,7 @@ package dasel
 
 import (
 	"fmt"
+	"github.com/tomwright/dasel/v2/dencoding"
 	"reflect"
 )
 
@@ -27,7 +28,13 @@ var AllFunc = BasicFunction{
 					res = append(res, val.MapIndex(key))
 				}
 			default:
-				return nil, fmt.Errorf("cannot use all selector on non slice/array/map types")
+				if val.IsDencodingMap() {
+					for _, k := range val.Interface().(*dencoding.Map).Keys() {
+						res = append(res, val.dencodingMapIndex(ValueOf(k)))
+					}
+				} else {
+					return nil, fmt.Errorf("cannot use all selector on non slice/array/map types")
+				}
 			}
 		}
 
diff --git a/func_equal.go b/func_equal.go
index 01dbb26..beec504 100644
--- a/func_equal.go
+++ b/func_equal.go
@@ -2,6 +2,7 @@ package dasel
 
 import (
 	"fmt"
+	"github.com/tomwright/dasel/v2/util"
 	"reflect"
 )
 
@@ -51,7 +52,7 @@ var EqualFunc = BasicFunction{
 				return false, nil
 			}
 
-			gotValue := fmt.Sprint(gotValues[0].Interface())
+			gotValue := util.ToString(gotValues[0].Interface())
 			return gotValue == cmp.value, nil
 		}
 
diff --git a/func_join.go b/func_join.go
index f106def..cdc56e1 100644
--- a/func_join.go
+++ b/func_join.go
@@ -1,7 +1,7 @@
 package dasel
 
 import (
-	"fmt"
+	"github.com/tomwright/dasel/v2/util"
 	"strings"
 )
 
@@ -22,7 +22,7 @@ var JoinFunc = BasicFunction{
 
 			res := make([]string, len(gotValues))
 			for k, v := range gotValues {
-				res[k] = fmt.Sprint(v.Interface())
+				res[k] = util.ToString(v.Interface())
 			}
 			return res, nil
 		}
@@ -36,7 +36,7 @@ var JoinFunc = BasicFunction{
 		if len(args) == 0 {
 			values := make([]string, len(input))
 			for k, v := range input {
-				values[k] = fmt.Sprint(v.Interface())
+				values[k] = util.ToString(v.Interface())
 			}
 			res = append(res, ValueOf(strings.Join(values, separator)))
 			return res, nil
diff --git a/func_less_than.go b/func_less_than.go
index c7673f3..7000dcf 100644
--- a/func_less_than.go
+++ b/func_less_than.go
@@ -2,6 +2,7 @@ package dasel
 
 import (
 	"fmt"
+	"github.com/tomwright/dasel/v2/util"
 	"reflect"
 	"sort"
 )
@@ -49,7 +50,7 @@ var LessThanFunc = BasicFunction{
 				return false, nil
 			}
 
-			gotValue := fmt.Sprint(gotValues[0].Interface())
+			gotValue := util.ToString(gotValues[0].Interface())
 
 			// The values are equal
 			if gotValue == cmp.value {
diff --git a/func_more_than.go b/func_more_than.go
index c4c43d4..73ff017 100644
--- a/func_more_than.go
+++ b/func_more_than.go
@@ -2,6 +2,7 @@ package dasel
 
 import (
 	"fmt"
+	"github.com/tomwright/dasel/v2/util"
 	"reflect"
 	"sort"
 )
@@ -49,7 +50,7 @@ var MoreThanFunc = BasicFunction{
 				return false, nil
 			}
 
-			gotValue := fmt.Sprint(gotValues[0].Interface())
+			gotValue := util.ToString(gotValues[0].Interface())
 
 			// The values are equal
 			if gotValue == cmp.value {
diff --git a/func_property.go b/func_property.go
index 4a245e1..9d9be87 100644
--- a/func_property.go
+++ b/func_property.go
@@ -35,7 +35,7 @@ var PropertyFunc = BasicFunction{
 		input := s.inputs()
 
 		if c.CreateWhenMissing() {
-			input = input.initEmptyMaps()
+			input = input.initEmptydencodingMaps()
 		}
 
 		res := make(Values, 0)
@@ -70,7 +70,21 @@ var PropertyFunc = BasicFunction{
 					}
 					res = append(res, value)
 				default:
-					return nil, fmt.Errorf("cannot use property selector on non map/struct types: %s: %w", val.Kind().String(), &ErrPropertyNotFound{Property: property})
+					if val.IsDencodingMap() {
+						index := val.dencodingMapIndex(ValueOf(property))
+						if index.IsEmpty() {
+							if isOptional {
+								continue
+							}
+							if !c.CreateWhenMissing() {
+								return nil, fmt.Errorf("could not access map index: %w", &ErrPropertyNotFound{Property: property})
+							}
+							index = index.asUninitialised()
+						}
+						res = append(res, index)
+					} else {
+						return nil, fmt.Errorf("cannot use property selector on non map/struct types: %s: %w", val.Kind().String(), &ErrPropertyNotFound{Property: property})
+					}
 				}
 			}
 		}
diff --git a/func_string.go b/func_string.go
index 203c2f2..295071b 100644
--- a/func_string.go
+++ b/func_string.go
@@ -1,8 +1,6 @@
 package dasel
 
-import (
-	"fmt"
-)
+import "github.com/tomwright/dasel/v2/util"
 
 var StringFunc = BasicFunction{
 	name: "string",
@@ -16,7 +14,7 @@ var StringFunc = BasicFunction{
 		res := make(Values, len(input))
 
 		for k, v := range args {
-			res[k] = ValueOf(fmt.Sprint(v))
+			res[k] = ValueOf(util.ToString(v))
 		}
 
 		return res, nil
diff --git a/func_type.go b/func_type.go
index 1771a50..3d0b119 100644
--- a/func_type.go
+++ b/func_type.go
@@ -15,19 +15,24 @@ var TypeFunc = BasicFunction{
 
 		for _, val := range input {
 			resStr := "unknown"
-			switch val.Kind() {
-			case reflect.Slice, reflect.Array:
-				resStr = "array"
-			case reflect.Map, reflect.Struct:
+
+			if val.IsDencodingMap() {
 				resStr = "object"
-			case reflect.String:
-				resStr = "string"
-			case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64,
-				reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64,
-				reflect.Float32, reflect.Float64:
-				resStr = "number"
-			case reflect.Bool:
-				resStr = "bool"
+			} else {
+				switch val.Kind() {
+				case reflect.Slice, reflect.Array:
+					resStr = "array"
+				case reflect.Map, reflect.Struct:
+					resStr = "object"
+				case reflect.String:
+					resStr = "string"
+				case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64,
+					reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64,
+					reflect.Float32, reflect.Float64:
+					resStr = "number"
+				case reflect.Bool:
+					resStr = "bool"
+				}
 			}
 			res = append(res, ValueOf(resStr))
 		}
diff --git a/go.mod b/go.mod
index 089afed..ddb6460 100644
--- a/go.mod
+++ b/go.mod
@@ -5,11 +5,11 @@ go 1.18
 require (
 	github.com/alecthomas/chroma v0.10.0
 	github.com/clbanning/mxj/v2 v2.5.7
-	github.com/pelletier/go-toml v1.9.5
 	github.com/spf13/cobra v1.6.1
 	golang.org/x/net v0.8.0
 	golang.org/x/text v0.8.0
-	gopkg.in/yaml.v2 v2.4.0
+	gopkg.in/yaml.v3 v3.0.1
+	github.com/pelletier/go-toml/v2 v2.0.6
 )
 
 require (
diff --git a/go.sum b/go.sum
index f80bbc0..ffd56d2 100644
--- a/go.sum
+++ b/go.sum
@@ -4,15 +4,13 @@ github.com/clbanning/mxj/v2 v2.5.7 h1:7q5lvUpaPF/WOkqgIDiwjBJaznaLCCBd78pi8ZyAnE
 github.com/clbanning/mxj/v2 v2.5.7/go.mod h1:hNiWqW14h+kc+MdF9C6/YoRfjEJoR3ou6tn/Qo+ve2s=
 github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
-github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
 github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 github.com/dlclark/regexp2 v1.4.0 h1:F1rxgk7p4uKjwIQxBs9oAXe5CqrXlCduYEJvrF4u93E=
 github.com/dlclark/regexp2 v1.4.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc=
 github.com/inconshreveable/mousetrap v1.0.1 h1:U3uMjPSQEBMNp1lFxmllqCPM6P5u/Xq7Pgzkat/bFNc=
 github.com/inconshreveable/mousetrap v1.0.1/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
-github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8=
-github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c=
-github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
+github.com/pelletier/go-toml/v2 v2.0.6 h1:nrzqCb7j9cDFj2coyLNLaZuJTLjWjlaz6nvTvIwycIU=
+github.com/pelletier/go-toml/v2 v2.0.6/go.mod h1:eumQOmlWiOPt5WriQQqoM5y18pDHwha2N+QD+EUNTek=
 github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
 github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
 github.com/spf13/cobra v1.6.1 h1:o94oiPyS4KD1mPy2fmcYYHHfCxLqYjJOhGsCHFZtEzA=
@@ -20,16 +18,17 @@ github.com/spf13/cobra v1.6.1/go.mod h1:IOw/AERYS7UzyrGinqmz6HLUo219MORXGxhbaJUq
 github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
 github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
 github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
-github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
+github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
+github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
 github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
+github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
 golang.org/x/net v0.8.0 h1:Zrh2ngAOFYneWTAIAPethzeaQLuHwhuBkuV6ZiRnUaQ=
 golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
 golang.org/x/text v0.8.0 h1:57P1ETyNKtuIjB4SRd15iJxuhj8Gc416Y78H3qgMh68=
 golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
-gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
-gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
-gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
 gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
 gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
 gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
diff --git a/storage/csv.go b/storage/csv.go
index b684885..a2f888e 100644
--- a/storage/csv.go
+++ b/storage/csv.go
@@ -5,6 +5,8 @@ import (
 	"encoding/csv"
 	"fmt"
 	"github.com/tomwright/dasel/v2"
+	"github.com/tomwright/dasel/v2/dencoding"
+	"github.com/tomwright/dasel/v2/util"
 	"sort"
 )
 
@@ -75,13 +77,19 @@ func interfaceToCSVDocument(val interface{}) (*CSVDocument, error) {
 			Headers: headers,
 		}, nil
 
+	case *dencoding.Map:
+		return &CSVDocument{
+			Value:   []map[string]any{v.UnorderedData()},
+			Headers: v.Keys(),
+		}, nil
+
 	case []interface{}:
 		mapVals := make([]map[string]interface{}, 0)
 		headers := make([]string, 0)
 		for _, val := range v {
-			if x, ok := val.(map[string]interface{}); ok {
+			switch x := val.(type) {
+			case map[string]any:
 				mapVals = append(mapVals, x)
-
 				for objectKey := range x {
 					found := false
 					for _, existingHeader := range headers {
@@ -94,6 +102,20 @@ func interfaceToCSVDocument(val interface{}) (*CSVDocument, error) {
 						headers = append(headers, objectKey)
 					}
 				}
+			case *dencoding.Map:
+				mapVals = append(mapVals, x.UnorderedData())
+				for _, objectKey := range x.Keys() {
+					found := false
+					for _, existingHeader := range headers {
+						if existingHeader == objectKey {
+							found = true
+							break
+						}
+					}
+					if !found {
+						headers = append(headers, objectKey)
+					}
+				}
 			}
 		}
 		sort.Strings(headers)
@@ -183,12 +205,7 @@ func (p *CSVParser) toBytesHandleDoc(writer *csv.Writer, doc *CSVDocument) error
 			if !ok {
 				val = ""
 			}
-			switch val.(type) {
-			case float32, float64:
-				values = append(values, fmt.Sprintf("%f", val))
-			default:
-				values = append(values, fmt.Sprint(val))
-			}
+			values = append(values, util.ToString(val))
 		}
 
 		if err := writer.Write(values); err != nil {
diff --git a/storage/json.go b/storage/json.go
index a1f8778..8e4c110 100644
--- a/storage/json.go
+++ b/storage/json.go
@@ -2,9 +2,9 @@ package storage
 
 import (
 	"bytes"
-	"encoding/json"
 	"fmt"
 	"github.com/tomwright/dasel/v2"
+	"github.com/tomwright/dasel/v2/dencoding"
 	"io"
 )
 
@@ -19,20 +19,21 @@ type JSONParser struct {
 
 // FromBytes returns some data that is represented by the given bytes.
 func (p *JSONParser) FromBytes(byteData []byte) (dasel.Value, error) {
-	res := make([]interface{}, 0)
+	res := make([]any, 0)
 
-	decoder := json.NewDecoder(bytes.NewBuffer(byteData))
+	decoder := dencoding.NewJSONDecoder(bytes.NewReader(byteData))
 
 docLoop:
 	for {
-		var docData interface{}
+		var docData any
 		if err := decoder.Decode(&docData); err != nil {
 			if err == io.EOF {
 				break docLoop
 			}
 			return dasel.Value{}, fmt.Errorf("could not unmarshal data: %w", err)
+		} else {
+			res = append(res, docData)
 		}
-		res = append(res, docData)
 	}
 
 	switch len(res) {
@@ -47,40 +48,62 @@ docLoop:
 	}
 }
 
-// ToBytes returns a slice of bytes that represents the given value.
-func (p *JSONParser) ToBytes(value dasel.Value, options ...ReadWriteOption) ([]byte, error) {
-	buffer := new(bytes.Buffer)
-	encoder := json.NewEncoder(buffer)
+type toBytesOptions struct {
+	indent      string
+	prefix      string
+	prettyPrint bool
+	colourise   bool
+	escapeHTML  bool
+}
 
-	indent := "  "
-	prettyPrint := true
-	colourise := false
+func getToBytesOptions(options ...ReadWriteOption) toBytesOptions {
+	res := toBytesOptions{
+		indent:      "  ",
+		prefix:      "",
+		prettyPrint: true,
+		colourise:   false,
+		escapeHTML:  false,
+	}
 
 	for _, o := range options {
 		switch o.Key {
 		case OptionIndent:
 			if value, ok := o.Value.(string); ok {
-				indent = value
+				res.indent = value
 			}
 		case OptionPrettyPrint:
 			if value, ok := o.Value.(bool); ok {
-				prettyPrint = value
+				res.prettyPrint = value
 			}
 		case OptionColourise:
 			if value, ok := o.Value.(bool); ok {
-				colourise = value
+				res.colourise = value
 			}
 		case OptionEscapeHTML:
 			if value, ok := o.Value.(bool); ok {
-				encoder.SetEscapeHTML(value)
+				res.escapeHTML = value
 			}
 		}
 	}
 
-	if !prettyPrint {
-		indent = ""
+	if !res.prettyPrint {
+		res.indent = ""
 	}
-	encoder.SetIndent("", indent)
+
+	return res
+}
+
+// ToBytes returns a slice of bytes that represents the given value.
+func (p *JSONParser) ToBytes(value dasel.Value, options ...ReadWriteOption) ([]byte, error) {
+	encoderOptions := make([]dencoding.JSONEncoderOption, 0)
+
+	baseOptions := getToBytesOptions(options...)
+	encoderOptions = append(encoderOptions, dencoding.JSONEscapeHTML(baseOptions.escapeHTML))
+	encoderOptions = append(encoderOptions, dencoding.JSONEncodeIndent(baseOptions.prefix, baseOptions.indent))
+
+	buffer := new(bytes.Buffer)
+	encoder := dencoding.NewJSONEncoder(buffer, encoderOptions...)
+	defer encoder.Close()
 
 	switch {
 	case value.Metadata("isSingleDocument") == true:
@@ -99,7 +122,7 @@ func (p *JSONParser) ToBytes(value dasel.Value, options ...ReadWriteOption) ([]b
 		}
 	}
 
-	if colourise {
+	if baseOptions.colourise {
 		if err := ColouriseBuffer(buffer, "json"); err != nil {
 			return nil, fmt.Errorf("could not colourise output: %w", err)
 		}
diff --git a/storage/toml.go b/storage/toml.go
index 8c31c05..c545cb3 100644
--- a/storage/toml.go
+++ b/storage/toml.go
@@ -3,8 +3,9 @@ package storage
 import (
 	"bytes"
 	"fmt"
-	"github.com/pelletier/go-toml"
 	"github.com/tomwright/dasel/v2"
+	"github.com/tomwright/dasel/v2/dencoding"
+	"io"
 )
 
 func init() {
@@ -18,69 +19,80 @@ type TOMLParser struct {
 
 // FromBytes returns some data that is represented by the given bytes.
 func (p *TOMLParser) FromBytes(byteData []byte) (dasel.Value, error) {
-	var data interface{}
-	if err := toml.Unmarshal(byteData, &data); err != nil {
-		return dasel.Value{}, fmt.Errorf("could not unmarshal data: %w", err)
+	res := make([]interface{}, 0)
+
+	decoder := dencoding.NewTOMLDecoder(bytes.NewReader(byteData))
+
+docLoop:
+	for {
+		var docData interface{}
+		if err := decoder.Decode(&docData); err != nil {
+			if err == io.EOF {
+				break docLoop
+			}
+			return dasel.Value{}, fmt.Errorf("could not unmarshal data: %w", err)
+		}
+
+		formattedDocData := cleanupYamlMapValue(docData)
+
+		res = append(res, formattedDocData)
+	}
+	switch len(res) {
+	case 0:
+		return dasel.Value{}, nil
+	case 1:
+		return dasel.ValueOf(res[0]).WithMetadata("isSingleDocument", true), nil
+	default:
+		return dasel.ValueOf(res).WithMetadata("isMultiDocument", true), nil
 	}
-	return dasel.ValueOf(data).WithMetadata("isSingleDocument", true), nil
 }
 
 // ToBytes returns a slice of bytes that represents the given value.
 func (p *TOMLParser) ToBytes(value dasel.Value, options ...ReadWriteOption) ([]byte, error) {
-	buf := new(bytes.Buffer)
-
-	enc := toml.NewEncoder(buf)
+	buffer := new(bytes.Buffer)
 
 	colourise := false
 
+	encoderOptions := make([]dencoding.TOMLEncoderOption, 0)
+
 	for _, o := range options {
 		switch o.Key {
-		case OptionIndent:
-			if indent, ok := o.Value.(string); ok {
-				enc.Indentation(indent)
-			}
 		case OptionColourise:
 			if value, ok := o.Value.(bool); ok {
 				colourise = value
 			}
+		case OptionIndent:
+			if value, ok := o.Value.(string); ok {
+				encoderOptions = append(encoderOptions, dencoding.TOMLIndentSymbol(value))
+			}
 		}
 	}
 
+	encoder := dencoding.NewTOMLEncoder(buffer, encoderOptions...)
+	defer encoder.Close()
+
 	switch {
 	case value.Metadata("isSingleDocument") == true:
-		if err := enc.Encode(value.Interface()); err != nil {
-			if err.Error() == "Only a struct or map can be marshaled to TOML" {
-				buf.Write([]byte(fmt.Sprintf("%v\n", value.Interface())))
-			} else {
-				return nil, err
-			}
+		if err := encoder.Encode(value.Interface()); err != nil {
+			return nil, fmt.Errorf("could not encode single document: %w", err)
 		}
 	case value.Metadata("isMultiDocument") == true:
 		for i := 0; i < value.Len(); i++ {
-			field := value.Index(i)
-			if err := enc.Encode(field.Interface()); err != nil {
-				if err.Error() == "Only a struct or map can be marshaled to TOML" {
-					buf.Write([]byte(fmt.Sprintf("%v\n", field.Interface())))
-				} else {
-					return nil, err
-				}
+			if err := encoder.Encode(value.Index(i).Interface()); err != nil {
+				return nil, fmt.Errorf("could not encode multi document [%d]: %w", i, err)
 			}
 		}
 	default:
-		if err := enc.Encode(value.Interface()); err != nil {
-			if err.Error() == "Only a struct or map can be marshaled to TOML" {
-				buf.Write([]byte(fmt.Sprintf("%v\n", value.Interface())))
-			} else {
-				return nil, err
-			}
+		if err := encoder.Encode(value.Interface()); err != nil {
+			return nil, fmt.Errorf("could not encode default document type: %w", err)
 		}
 	}
 
 	if colourise {
-		if err := ColouriseBuffer(buf, "toml"); err != nil {
+		if err := ColouriseBuffer(buffer, "toml"); err != nil {
 			return nil, fmt.Errorf("could not colourise output: %w", err)
 		}
 	}
 
-	return buf.Bytes(), nil
+	return buffer.Bytes(), nil
 }
diff --git a/storage/yaml.go b/storage/yaml.go
index 68ad3b6..f6a8cd1 100644
--- a/storage/yaml.go
+++ b/storage/yaml.go
@@ -4,7 +4,8 @@ import (
 	"bytes"
 	"fmt"
 	"github.com/tomwright/dasel/v2"
-	"gopkg.in/yaml.v2"
+	"github.com/tomwright/dasel/v2/dencoding"
+	"github.com/tomwright/dasel/v2/util"
 	"io"
 )
 
@@ -21,7 +22,7 @@ type YAMLParser struct {
 func (p *YAMLParser) FromBytes(byteData []byte) (dasel.Value, error) {
 	res := make([]interface{}, 0)
 
-	decoder := yaml.NewDecoder(bytes.NewBuffer(byteData))
+	decoder := dencoding.NewYAMLDecoder(bytes.NewReader(byteData))
 
 docLoop:
 	for {
@@ -58,7 +59,7 @@ func cleanupYamlInterfaceArray(in []interface{}) []interface{} {
 func cleanupYamlInterfaceMap(in map[interface{}]interface{}) map[string]interface{} {
 	res := make(map[string]interface{})
 	for k, v := range in {
-		res[fmt.Sprint(k)] = cleanupYamlMapValue(v)
+		res[util.ToString(k)] = cleanupYamlMapValue(v)
 	}
 	return res
 }
@@ -79,20 +80,27 @@ func cleanupYamlMapValue(v interface{}) interface{} {
 // ToBytes returns a slice of bytes that represents the given value.
 func (p *YAMLParser) ToBytes(value dasel.Value, options ...ReadWriteOption) ([]byte, error) {
 	buffer := new(bytes.Buffer)
-	encoder := yaml.NewEncoder(buffer)
-	defer encoder.Close()
 
 	colourise := false
 
+	encoderOptions := make([]dencoding.YAMLEncoderOption, 0)
+
 	for _, o := range options {
 		switch o.Key {
 		case OptionColourise:
 			if value, ok := o.Value.(bool); ok {
 				colourise = value
 			}
+		case OptionIndent:
+			if value, ok := o.Value.(int); ok {
+				encoderOptions = append(encoderOptions, dencoding.YAMLEncodeIndent(value))
+			}
 		}
 	}
 
+	encoder := dencoding.NewYAMLEncoder(buffer, encoderOptions...)
+	defer encoder.Close()
+
 	switch {
 	case value.Metadata("isSingleDocument") == true:
 		if err := encoder.Encode(value.Interface()); err != nil {
diff --git a/util/to_string.go b/util/to_string.go
new file mode 100644
index 0000000..04958ec
--- /dev/null
+++ b/util/to_string.go
@@ -0,0 +1,26 @@
+package util
+
+import (
+	"fmt"
+)
+
+// ToString converts the given value to a string.
+func ToString(value any) string {
+	switch v := value.(type) {
+	case nil:
+		return "null"
+	case string:
+		return v
+	case []byte:
+		return string(v)
+	case int, int8, int16, int32, int64,
+		uint, uint8, uint16, uint32, uint64:
+		return fmt.Sprintf("%d", v)
+	case float32, float64:
+		return fmt.Sprintf("%f", v)
+	case bool:
+		return fmt.Sprint(v)
+	default:
+		return fmt.Sprint(v)
+	}
+}
diff --git a/value.go b/value.go
index b36ff73..03c6ffa 100644
--- a/value.go
+++ b/value.go
@@ -1,7 +1,8 @@
 package dasel
 
 import (
-	"fmt"
+	"github.com/tomwright/dasel/v2/dencoding"
+	"github.com/tomwright/dasel/v2/util"
 	"reflect"
 )
 
@@ -66,7 +67,7 @@ func (v Value) Len() int {
 			return 0
 		}
 	default:
-		return len(fmt.Sprint(v.Interface()))
+		return len(util.ToString(v.Interface()))
 	}
 }
 
@@ -96,15 +97,26 @@ func containsKind(kinds []reflect.Kind, kind reflect.Kind) bool {
 	return false
 }
 
+var dencodingMapType = reflect.TypeOf(&dencoding.Map{})
+
+func isdencodingMap(value reflect.Value) bool {
+	return value.Kind() == reflect.Ptr && value.Type() == dencodingMapType
+}
+
 func unpackReflectValue(value reflect.Value, kinds ...reflect.Kind) reflect.Value {
 	if len(kinds) == 0 {
 		kinds = append(kinds, reflect.Ptr, reflect.Interface)
 	}
 	res := value
-	for containsKind(kinds, res.Kind()) {
+	for {
+		if isdencodingMap(res) {
+			return res
+		}
+		if !containsKind(kinds, res.Kind()) {
+			return res
+		}
 		res = res.Elem()
 	}
-	return res
 }
 
 func (v Value) FirstAddressable() reflect.Value {
@@ -144,6 +156,42 @@ func (v Value) Delete() {
 	panic("unable to delete value with missing deleteFn")
 }
 
+func (v Value) IsDencodingMap() bool {
+	if v.Kind() != reflect.Ptr {
+		return false
+	}
+	_, ok := v.Interface().(*dencoding.Map)
+	return ok
+}
+
+func (v Value) dencodingMapIndex(key Value) Value {
+	getValueByKey := func() reflect.Value {
+		if !v.IsDencodingMap() {
+			return reflect.Value{}
+		}
+		om := v.Interface().(*dencoding.Map)
+		if v, ok := om.Get(key.Value.String()); !ok {
+			return reflect.Value{}
+		} else {
+			return reflect.ValueOf(v)
+		}
+	}
+	index := Value{
+		Value: getValueByKey(),
+		setFn: func(value Value) {
+			// Note that we do not use Interface() here as it will dereference the received value.
+			// Instead, we only dereference the interface type to receive the pointer.
+			v.Interface().(*dencoding.Map).Set(key.Value.String(), value.Unpack(reflect.Interface).Interface())
+		},
+		deleteFn: func() {
+			v.Interface().(*dencoding.Map).Delete(key.Value.String())
+		},
+	}
+	return index.
+		WithMetadata("key", key.Interface()).
+		WithMetadata("parent", v)
+}
+
 // MapIndex returns the value associated with key in the map v.
 // It returns the zero Value if no field was found.
 func (v Value) MapIndex(key Value) Value {
@@ -263,6 +311,14 @@ func (v Value) initEmptyMap() Value {
 	return v
 }
 
+func (v Value) initEmptydencodingMap() Value {
+	om := dencoding.NewMap()
+	rom := reflect.ValueOf(om)
+	v.Set(Value{Value: rom})
+	v.Value = rom
+	return v
+}
+
 func (v Value) initEmptySlice() Value {
 	emptySlice := reflect.MakeSlice(sliceInterfaceType, 0, 0)
 
@@ -328,6 +384,14 @@ func makeAddressableMap(value reflect.Value) reflect.Value {
 func makeAddressable(value reflect.Value) reflect.Value {
 	unpacked := unpackReflectValue(value)
 
+	if isdencodingMap(unpacked) {
+		om := value.Interface().(*dencoding.Map)
+		for _, kv := range om.KeyValues() {
+			om.Set(kv.Key, makeAddressable(reflect.ValueOf(kv.Value)).Interface())
+		}
+		return value
+	}
+
 	switch unpacked.Kind() {
 	case reflect.Slice:
 		return makeAddressableSlice(value)
@@ -367,6 +431,14 @@ func derefMap(value reflect.Value) reflect.Value {
 func deref(value reflect.Value) reflect.Value {
 	unpacked := unpackReflectValue(value)
 
+	if isdencodingMap(unpacked) {
+		om := value.Interface().(*dencoding.Map)
+		for _, kv := range om.KeyValues() {
+			om.Set(kv.Key, deref(reflect.ValueOf(kv.Value)).Interface())
+		}
+		return value
+	}
+
 	switch unpacked.Kind() {
 	case reflect.Slice:
 		return derefSlice(value)
@@ -389,11 +461,23 @@ func (v Values) Interfaces() []interface{} {
 	return res
 }
 
-func (v Values) initEmptyMaps() Values {
+//func (v Values) initEmptyMaps() Values {
+//	res := make(Values, len(v))
+//	for k, value := range v {
+//		if value.IsEmpty() {
+//			res[k] = value.initEmptyMap()
+//		} else {
+//			res[k] = value
+//		}
+//	}
+//	return res
+//}
+
+func (v Values) initEmptydencodingMaps() Values {
 	res := make(Values, len(v))
 	for k, value := range v {
 		if value.IsEmpty() {
-			res[k] = value.initEmptyMap()
+			res[k] = value.initEmptydencodingMap()
 		} else {
 			res[k] = value
 		}


Test patch:
diff --git a/dencoding/json_decoder_test.go b/dencoding/json_decoder_test.go
new file mode 100644
index 0000000..2aca6ac
--- /dev/null
+++ b/dencoding/json_decoder_test.go
@@ -0,0 +1,53 @@
+package dencoding_test
+
+import (
+	"bytes"
+	"github.com/tomwright/dasel/v2/dencoding"
+	"io"
+	"reflect"
+	"testing"
+)
+
+func TestJSONDecoder_Decode(t *testing.T) {
+	b := []byte(`{"x":1,"a":"hello"}{"x":2,"a":"there"}{"a":"Tom","x":3}`)
+	dec := dencoding.NewJSONDecoder(bytes.NewReader(b))
+
+	maps := make([]any, 0)
+	for {
+		var v any
+		if err := dec.Decode(&v); err != nil {
+			if err == io.EOF {
+				break
+			}
+			t.Errorf("unexpected error: %v", err)
+			return
+		}
+		maps = append(maps, v)
+	}
+
+	exp := [][]dencoding.KeyValue{
+		{
+			{Key: "x", Value: int64(1)},
+			{Key: "a", Value: "hello"},
+		},
+		{
+			{Key: "x", Value: int64(2)},
+			{Key: "a", Value: "there"},
+		},
+		{
+			{Key: "a", Value: "Tom"},
+			{Key: "x", Value: int64(3)},
+		},
+	}
+
+	got := make([][]dencoding.KeyValue, 0)
+	for _, v := range maps {
+		if m, ok := v.(*dencoding.Map); ok {
+			got = append(got, m.KeyValues())
+		}
+	}
+
+	if !reflect.DeepEqual(exp, got) {
+		t.Errorf("expected %v, got %v", exp, got)
+	}
+}
diff --git a/dencoding/json_encoder_test.go b/dencoding/json_encoder_test.go
new file mode 100644
index 0000000..513e183
--- /dev/null
+++ b/dencoding/json_encoder_test.go
@@ -0,0 +1,35 @@
+package dencoding_test
+
+import (
+	"bytes"
+	"github.com/tomwright/dasel/v2/dencoding"
+	"testing"
+)
+
+func TestJSONEncoder_Encode(t *testing.T) {
+	orig := dencoding.NewMap().
+		Set("c", "x").
+		Set("b", "y").
+		Set("a", "z")
+
+	exp := `{
+  "c": "x",
+  "b": "y",
+  "a": "z"
+}
+`
+
+	gotBuffer := new(bytes.Buffer)
+
+	encoder := dencoding.NewJSONEncoder(gotBuffer, dencoding.JSONEncodeIndent("", "  "))
+	if err := encoder.Encode(orig); err != nil {
+		t.Errorf("unexpected error: %v", err)
+		return
+	}
+
+	got := gotBuffer.String()
+
+	if exp != got {
+		t.Errorf("expected %s, got %s", exp, got)
+	}
+}
diff --git a/dencoding/toml_decoder_test.go b/dencoding/toml_decoder_test.go
new file mode 100644
index 0000000..6ade297
--- /dev/null
+++ b/dencoding/toml_decoder_test.go
@@ -0,0 +1,79 @@
+package dencoding_test
+
+import (
+	"bytes"
+	"github.com/tomwright/dasel/v2/dencoding"
+	"io"
+	"reflect"
+	"testing"
+)
+
+func TestTOMLDecoder_Decode(t *testing.T) {
+
+	t.Run("KeyValue", func(t *testing.T) {
+		b := []byte(`x = 1
+a = 'hello'`)
+		dec := dencoding.NewTOMLDecoder(bytes.NewReader(b))
+
+		maps := make([]any, 0)
+		for {
+			var v any
+			if err := dec.Decode(&v); err != nil {
+				if err == io.EOF {
+					break
+				}
+				t.Errorf("unexpected error: %v", err)
+				return
+			}
+			maps = append(maps, v)
+		}
+
+		exp := []any{
+			map[string]any{
+				"x": int64(1),
+				"a": "hello",
+			},
+		}
+
+		got := maps
+
+		if !reflect.DeepEqual(exp, got) {
+			t.Errorf("expected %v, got %v", exp, got)
+		}
+	})
+
+	t.Run("Table", func(t *testing.T) {
+		b := []byte(`
+[user]
+name = "Tom"
+age = 29
+`)
+		dec := dencoding.NewTOMLDecoder(bytes.NewReader(b))
+
+		got := make([]any, 0)
+		for {
+			var v any
+			if err := dec.Decode(&v); err != nil {
+				if err == io.EOF {
+					break
+				}
+				t.Errorf("unexpected error: %v", err)
+				return
+			}
+			got = append(got, v)
+		}
+
+		exp := []any{
+			map[string]any{
+				"user": map[string]any{
+					"age":  int64(29),
+					"name": "Tom",
+				},
+			},
+		}
+
+		if !reflect.DeepEqual(exp, got) {
+			t.Errorf("expected %v, got %v", exp, got)
+		}
+	})
+}
diff --git a/dencoding/toml_encoder_test.go b/dencoding/toml_encoder_test.go
new file mode 100644
index 0000000..eb66f74
--- /dev/null
+++ b/dencoding/toml_encoder_test.go
@@ -0,0 +1,33 @@
+package dencoding_test
+
+import (
+	"bytes"
+	"github.com/tomwright/dasel/v2/dencoding"
+	"testing"
+)
+
+func TestTOMLEncoder_Encode(t *testing.T) {
+	orig := dencoding.NewMap().
+		Set("c", "x").
+		Set("b", "y").
+		Set("a", []any{"a", "c", "b"})
+
+	exp := `a = ['a', 'c', 'b']
+b = 'y'
+c = 'x'
+`
+
+	gotBuffer := new(bytes.Buffer)
+
+	encoder := dencoding.NewTOMLEncoder(gotBuffer, dencoding.TOMLIndentSymbol("  "))
+	if err := encoder.Encode(orig); err != nil {
+		t.Errorf("unexpected error: %v", err)
+		return
+	}
+
+	got := gotBuffer.String()
+
+	if exp != got {
+		t.Errorf("expected %s, got %s", exp, got)
+	}
+}
diff --git a/dencoding/yaml_decoder_test.go b/dencoding/yaml_decoder_test.go
new file mode 100644
index 0000000..3d4f29f
--- /dev/null
+++ b/dencoding/yaml_decoder_test.go
@@ -0,0 +1,99 @@
+package dencoding_test
+
+import (
+	"bytes"
+	"github.com/tomwright/dasel/v2/dencoding"
+	"io"
+	"reflect"
+	"testing"
+)
+
+func TestYAMLDecoder_Decode(t *testing.T) {
+
+	t.Run("Basic", func(t *testing.T) {
+
+		b := []byte(`
+x: 1
+a: hello
+---
+x: 2
+a: there
+---
+a: Tom
+x: 3
+---`)
+		dec := dencoding.NewYAMLDecoder(bytes.NewReader(b))
+
+		maps := make([]any, 0)
+		for {
+			var v any
+			if err := dec.Decode(&v); err != nil {
+				if err == io.EOF {
+					break
+				}
+				t.Errorf("unexpected error: %v", err)
+				return
+			}
+			maps = append(maps, v)
+		}
+
+		exp := [][]dencoding.KeyValue{
+			{
+				{Key: "x", Value: int64(1)},
+				{Key: "a", Value: "hello"},
+			},
+			{
+				{Key: "x", Value: int64(2)},
+				{Key: "a", Value: "there"},
+			},
+			{
+				{Key: "a", Value: "Tom"},
+				{Key: "x", Value: int64(3)},
+			},
+		}
+
+		got := make([][]dencoding.KeyValue, 0)
+		for _, v := range maps {
+			if m, ok := v.(*dencoding.Map); ok {
+				got = append(got, m.KeyValues())
+			}
+		}
+
+		if !reflect.DeepEqual(exp, got) {
+			t.Errorf("expected %v, got %v", exp, got)
+		}
+	})
+
+	// https://github.com/TomWright/dasel/issues/278
+	t.Run("Issue278", func(t *testing.T) {
+		b := []byte(`
+key1: [value1,value2,value3,value4,value5]
+key2: value6
+`)
+		dec := dencoding.NewYAMLDecoder(bytes.NewReader(b))
+
+		got := make([]any, 0)
+		for {
+			var v any
+			if err := dec.Decode(&v); err != nil {
+				if err == io.EOF {
+					break
+				}
+				t.Errorf("unexpected error: %v", err)
+				return
+			}
+			got = append(got, v)
+		}
+
+		exp := []any{
+			dencoding.NewMap().
+				Set("key1", []any{"value1", "value2", "value3", "value4", "value5"}).
+				Set("key2", "value6"),
+		}
+
+		if !reflect.DeepEqual(exp, got) {
+			t.Errorf("expected %v, got %v", exp, got)
+		}
+	})
+
+}
diff --git a/dencoding/yaml_encoder_test.go b/dencoding/yaml_encoder_test.go
new file mode 100644
index 0000000..66061b0
--- /dev/null
+++ b/dencoding/yaml_encoder_test.go
@@ -0,0 +1,36 @@
+package dencoding_test
+
+import (
+	"bytes"
+	"github.com/tomwright/dasel/v2/dencoding"
+	"testing"
+)
+
+func TestYAMLEncoder_Encode(t *testing.T) {
+	orig := dencoding.NewMap().
+		Set("c", "x").
+		Set("b", "y").
+		Set("a", []any{"a", "c", "b"})
+
+	exp := `c: x
+b: y
+a:
+  - a
+  - c
+  - b
+`
+
+	gotBuffer := new(bytes.Buffer)
+
+	encoder := dencoding.NewYAMLEncoder(gotBuffer, dencoding.YAMLEncodeIndent(2))
+	if err := encoder.Encode(orig); err != nil {
+		t.Errorf("unexpected error: %v", err)
+		return
+	}
+
+	got := gotBuffer.String()
+
+	if exp != got {
+		t.Errorf("expected %s, got %s", exp, got)
+	}
+}
diff --git a/func_join_test.go b/func_join_test.go
index 95fb673..cf3147a 100644
--- a/func_join_test.go
+++ b/func_join_test.go
@@ -1,6 +1,7 @@
 package dasel
 
 import (
+	"github.com/tomwright/dasel/v2/dencoding"
 	"strings"
 	"testing"
 )
@@ -15,15 +16,13 @@ func TestJoinFunc(t *testing.T) {
 		}),
 	)
 
-	original := map[string]interface{}{
-		"name": map[string]interface{}{
-			"first": "Tom",
-			"last":  "Wright",
-		},
-		"colours": []interface{}{
+	original := dencoding.NewMap().
+		Set("name", dencoding.NewMap().
+			Set("first", "Tom").
+			Set("last", "Wright")).
+		Set("colours", []interface{}{
 			"red", "green", "blue",
-		},
-	}
+		})
 
 	t.Run(
 		"JoinCommaSeparator",
@@ -131,11 +130,10 @@ func TestJoinFunc(t *testing.T) {
 		"JoinManyLists",
 		selectTestAssert(
 			"all().join(\\,,all())",
-			map[string]interface{}{
-				"x": []interface{}{1, 2, 3},
-				"y": []interface{}{4, 5, 6},
-				"z": []interface{}{7, 8, 9},
-			},
+			dencoding.NewMap().
+				Set("x", []interface{}{1, 2, 3}).
+				Set("y", []interface{}{4, 5, 6}).
+				Set("z", []interface{}{7, 8, 9}),
 			func(t *testing.T, got []any) {
 				required := []string{"1", "2", "3", "4", "5", "6", "7", "8", "9"}
 				if len(got) != 1 {
diff --git a/storage/json_test.go b/storage/json_test.go
index 21c5df1..9547315 100644
--- a/storage/json_test.go
+++ b/storage/json_test.go
@@ -2,6 +2,7 @@ package storage_test
 
 import (
 	"github.com/tomwright/dasel/v2"
+	"github.com/tomwright/dasel/v2/dencoding"
 	"github.com/tomwright/dasel/v2/storage"
 	"reflect"
 	"testing"
@@ -11,9 +12,7 @@ var jsonBytes = []byte(`{
   "name": "Tom"
 }
 `)
-var jsonMap = map[string]interface{}{
-	"name": "Tom",
-}
+var jsonMap = dencoding.NewMap().Set("name", "Tom")
 
 func TestJSONParser_FromBytes(t *testing.T) {
 	t.Run("Valid", func(t *testing.T) {
@@ -169,9 +168,9 @@ var jsonBytesMulti = []byte(`{
 }
 `)
 
-var jsonMapMulti = []interface{}{
-	map[string]interface{}{"name": "Tom"},
-	map[string]interface{}{"name": "Ellis"},
+var jsonMapMulti = []any{
+	dencoding.NewMap().Set("name", "Tom"),
+	dencoding.NewMap().Set("name", "Ellis"),
 }
 
 var jsonBytesMultiMixed = []byte(`{
@@ -184,6 +183,6 @@ var jsonBytesMultiMixed = []byte(`{
 `)
 
 var jsonMapMultiMixed = []interface{}{
-	map[string]interface{}{"name": "Tom", "other": true},
-	map[string]interface{}{"name": "Ellis"},
+	dencoding.NewMap().Set("name", "Tom").Set("other", true),
+	dencoding.NewMap().Set("name", "Ellis"),
 }
diff --git a/storage/parser_test.go b/storage/parser_test.go
index 3a8cfd9..82bb14d 100644
--- a/storage/parser_test.go
+++ b/storage/parser_test.go
@@ -4,6 +4,7 @@ import (
 	"bytes"
 	"errors"
 	"github.com/tomwright/dasel/v2"
+	"github.com/tomwright/dasel/v2/dencoding"
 	"github.com/tomwright/dasel/v2/storage"
 	"reflect"
 	"strings"
@@ -171,27 +172,23 @@ func TestNewWriteParserFromFilename(t *testing.T) {
 	}
 }
 
-var jsonData = map[string]interface{}{
-	"name": "Tom",
-	"preferences": map[string]interface{}{
-		"favouriteColour": "red",
-	},
-	"colours": []interface{}{"red", "green", "blue"},
-	"colourCodes": []interface{}{
-		map[string]interface{}{
-			"name": "red",
-			"rgb":  "ff0000",
-		},
-		map[string]interface{}{
-			"name": "green",
-			"rgb":  "00ff00",
-		},
-		map[string]interface{}{
-			"name": "blue",
-			"rgb":  "0000ff",
-		},
-	},
-}
+var jsonData = dencoding.NewMap().
+	Set("name", "Tom").
+	Set("preferences", dencoding.NewMap().
+		Set("favouriteColour", "red"),
+	).
+	Set("colours", []any{"red", "green", "blue"}).
+	Set("colourCodes", []any{
+		dencoding.NewMap().
+			Set("name", "red").
+			Set("rgb", "ff0000"),
+		dencoding.NewMap().
+			Set("name", "green").
+			Set("rgb", "00ff00"),
+		dencoding.NewMap().
+			Set("name", "blue").
+			Set("rgb", "0000ff"),
+	})
 
 func TestLoadFromFile(t *testing.T) {
 	t.Run("ValidJSON", func(t *testing.T) {
@@ -200,9 +197,10 @@ func TestLoadFromFile(t *testing.T) {
 			t.Errorf("unexpected error: %s", err)
 			return
 		}
-		exp := jsonData
-		if !reflect.DeepEqual(exp, data.Interface()) {
-			t.Errorf("data does not match: exp %v, got %v", exp, data)
+		exp := jsonData.KeyValues()
+		got := data.Interface().(*dencoding.Map).KeyValues()
+		if !reflect.DeepEqual(exp, got) {
+			t.Errorf("data does not match: exp %v, got %v", exp, got)
 		}
 	})
 	t.Run("BaseFilePath", func(t *testing.T) {
diff --git a/storage/toml_test.go b/storage/toml_test.go
index 404dfff..6687f7b 100644
--- a/storage/toml_test.go
+++ b/storage/toml_test.go
@@ -8,10 +8,10 @@ import (
 	"testing"
 )
 
-var tomlBytes = []byte(`names = ["John", "Frank"]
+var tomlBytes = []byte(`names = ['John', 'Frank']
 
 [person]
-  name = "Tom"
+name = 'Tom'
 `)
 var tomlMap = map[string]interface{}{
 	"person": map[string]interface{}{
@@ -48,7 +48,7 @@ func TestTOMLParser_ToBytes(t *testing.T) {
 			return
 		}
 		if string(tomlBytes) != string(got) {
-			t.Errorf("expected:\n%s\ngot:\n%s", tomlBytes, got)
+			t.Errorf("expected:\n---\n%s\n---\ngot:\n---\n%s\n---", tomlBytes, got)
 		}
 	})
 	t.Run("SingleDocument", func(t *testing.T) {
@@ -71,7 +71,7 @@ func TestTOMLParser_ToBytes(t *testing.T) {
 		expBuf, _ := storage.Colourise(string(tomlBytes), "toml")
 		exp := expBuf.Bytes()
 		if !reflect.DeepEqual(exp, got) {
-			t.Errorf("expected %v, got %v", exp, got)
+			t.Errorf("expected %v, got %v", string(exp), string(got))
 		}
 	})
 	t.Run("SingleDocumentCustomIndent", func(t *testing.T) {
@@ -81,10 +81,10 @@ func TestTOMLParser_ToBytes(t *testing.T) {
 			return
 		}
 		got := string(res)
-		exp := `names = ["John", "Frank"]
+		exp := `names = ['John', 'Frank']
 
 [person]
-   name = "Tom"
+   name = 'Tom'
 `
 		if exp != got {
 			t.Errorf("expected:\n%s\ngot:\n%s", exp, got)
@@ -108,10 +108,10 @@ func TestTOMLParser_ToBytes(t *testing.T) {
 			t.Errorf("unexpected error: %s", err)
 			return
 		}
-		exp := `asd
+		exp := `'asd'
 `
 		if exp != string(got) {
-			t.Errorf("expected:\n%s\ngot:\n%s", exp, got)
+			t.Errorf("expected:\n---\n%s\n---\ngot:\n---\n%s\n---", exp, got)
 		}
 	})
 	t.Run("DefaultValue", func(t *testing.T) {
@@ -120,19 +120,19 @@ func TestTOMLParser_ToBytes(t *testing.T) {
 			t.Errorf("unexpected error: %s", err)
 			return
 		}
-		exp := `asd
+		exp := `'asd'
 `
 		if exp != string(got) {
 			t.Errorf("expected:\n%s\ngot:\n%s", exp, got)
 		}
 	})
 	t.Run("MultiDocumentValue", func(t *testing.T) {
-		got, err := (&storage.TOMLParser{}).ToBytes(dasel.ValueOf([]interface{}{"asd", "123"}).WithMetadata("isMultiDocument", true))
+		got, err := (&storage.TOMLParser{}).ToBytes(dasel.ValueOf([]interface{}{"asd", 123}).WithMetadata("isMultiDocument", true))
 		if err != nil {
 			t.Errorf("unexpected error: %s", err)
 			return
 		}
-		exp := `asd
+		exp := `'asd'
 123
 `
 		if exp != string(got) {
diff --git a/storage/yaml_test.go b/storage/yaml_test.go
index c8eea4f..53d8f8f 100644
--- a/storage/yaml_test.go
+++ b/storage/yaml_test.go
@@ -2,6 +2,7 @@ package storage_test
 
 import (
 	"github.com/tomwright/dasel/v2"
+	"github.com/tomwright/dasel/v2/dencoding"
 	"github.com/tomwright/dasel/v2/storage"
 	"reflect"
 	"strings"
@@ -10,39 +11,35 @@ import (
 
 var yamlBytes = []byte(`name: Tom
 numbers:
-- 1
-- 2
+    - 1
+    - 2
 `)
-var yamlMap = map[string]interface{}{
-	"name": "Tom",
-	"numbers": []interface{}{
-		1,
-		2,
-	},
-}
+var yamlMap = dencoding.NewMap().
+	Set("name", "Tom").
+	Set("numbers", []interface{}{
+		int64(1),
+		int64(2),
+	})
 
 var yamlBytesMulti = []byte(`name: Tom
 ---
 name: Jim
 `)
 var yamlMapMulti = []interface{}{
-	map[string]interface{}{
-		"name": "Tom",
-	},
-	map[string]interface{}{
-		"name": "Jim",
-	},
+	dencoding.NewMap().Set("name", "Tom"),
+	dencoding.NewMap().Set("name", "Jim"),
 }
 
 func TestYAMLParser_FromBytes(t *testing.T) {
 	t.Run("Valid", func(t *testing.T) {
-		got, err := (&storage.YAMLParser{}).FromBytes(yamlBytes)
+		gotFromBytes, err := (&storage.YAMLParser{}).FromBytes(yamlBytes)
 		if err != nil {
 			t.Errorf("unexpected error: %s", err)
 			return
 		}
-		exp := yamlMap
-		if !reflect.DeepEqual(exp, got.Interface()) {
+		exp := yamlMap.KeyValues()
+		got := gotFromBytes.Interface().(*dencoding.Map).KeyValues()
+		if !reflect.DeepEqual(exp, got) {
 			t.Errorf("expected %v, got %v", exp, got)
 		}
 	})
diff --git a/truthy_test.go b/truthy_test.go
index fd94ae9..8bca522 100644
--- a/truthy_test.go
+++ b/truthy_test.go
@@ -88,7 +88,7 @@ func TestIsTruthy(t *testing.T) {
 
 	for _, test := range testData {
 		tc := test
-		t.Run(fmt.Sprint(tc.name), func(t *testing.T) {
+		t.Run(tc.name, func(t *testing.T) {
 			if exp, got := tc.out, IsTruthy(tc.in); exp != got {
 				t.Errorf("expected %v, got %v", exp, got)
 			}