DEV Community

Cover image for Encoding and Decoding JSON in Dart
Mathieu Kerjouan
Mathieu Kerjouan

Posted on

Encoding and Decoding JSON in Dart

Is it really necessary to introduce JSON in 2026? Any developers had to deal with JSON at least one time in his career. Anyway, JSON (JavaScript Object Notation) is an open standard format designed for web development usage. In fine, it became one of the most used data format.

Before working on more challengin but interesting serializer like CBOR or Protocol Buffer, let take a moment to learn how use JSON in Dart.

import 'dart:convert';
Enter fullscreen mode Exit fullscreen mode

When one needs to encode or decode JSON, two ways existing, using the JsonCodec() class or use the specific class for encoding (JsonEncoder()) or for decoding (JsonDecoder()). In both case, the result will be the same.

Encoding

JSON encoder is directly integrated in the Dart SDK, and can be included in your code by simply importing dart:convert package.

import 'dart:convert';
Enter fullscreen mode Exit fullscreen mode

Let create a first function called encode() to encode any kind of object using the json.encode() interface.

void encode(Object data, {String label = ""}) {
  print("-- ${label}:");
  var encoded = json.encode(data);
  print(encoded);
  print("");
}
Enter fullscreen mode Exit fullscreen mode

Now, let create another function called encode2(). This one will instantiate an object from the JsonEncoder() class.

void encode2(Object data, {String label = ""}) {
  var encoder = JsonEncoder();
  print("-- ${label}:");
  var encoded = encoder.convert(data);
  print(encoded);
  print("");
}
Enter fullscreen mode Exit fullscreen mode

Great, we have two ways to encode our objects, the main entry-point is still missing though. Let fix that.

void main() {
  print("== encode with JsonCodec");
  encode(1, label: "integer(1)");
  encode("test", label: "string(test)");
  encode([], label: "list()");
  encode([1,2,3], label: "list(1,2,3)");
  encode({}, label: "map()");
  encode({"a": 1}, label: "map(a, 1)");
  encode(0.001, label: "float(0.001)");
  encode(true, label: "bool(true)");
  encode(false, label: "bool(false)");

  print("encode with JsonEncoder");
  encode2(1, label: "integer(1)");
  encode2("test", label: "string(test)");
  encode2([], label: "list()");
  encode2([1,2,3], label: "list(1,2,3)");
  encode2({}, label: "map()");
  encode2({"a": 1}, label: "map(a, 1)");
  encode2(0.001, label: "float(0.001)");
  encode2(true, label: "bool(true)");
  encode2(false, label: "bool(false)");
}
Enter fullscreen mode Exit fullscreen mode

The time has come to execute this program and see the result.

$ dart run bin/encode.dart
== encode with JsonCodec
-- integer(1):
1

-- string(test):
"test"

-- list():
[]

-- list(1,2,3):
[1,2,3]

-- map():
{}

-- map(a, 1):
{"a":1}

-- float(0.001):
0.001

-- bool(true):
true

-- bool(false):
false

encode with JsonEncoder
-- integer(1):
1

-- string(test):
"test"

-- list():
[]

-- list(1,2,3):
[1,2,3]

-- map():
{}

-- map(a, 1):
{"a":1}

-- float(0.001):
0.001

-- bool(true):
true

-- bool(false):
false
Enter fullscreen mode Exit fullscreen mode

It looks okay, the main Dart types are supported, but what will happen if we give an unsupported type to the encoder?

void main() {
  json.encode(Object());
}
Enter fullscreen mode Exit fullscreen mode

The code is simple, we pass a raw Object() to json.encode().

$ dart bin/encode_exception.dart
Uncaught Error, error: Error: Converting object to an encodable object failed: Instance of 'Object'
Enter fullscreen mode Exit fullscreen mode

Doh, it produces an error! In fact, this is normal, the JSON encoder does not know how to deal with such objects. JSON only support a subset of all Dart type, so, in this case, we need to find a way to convert (serialize) similar objects to something compatible with JSON. In most of the case, it will be converted as String(), perhaps using Base64 or something similar.

To illustrate that, let create a new class called Tuple, it will contain two public attributes, a left and a right one. Both are String(). To represent this kind of object in JSON we can easily use a Map containing at least two fields, one for the left and one of the right. Adding a method called toJson() here will help us.

class Tuple {
  String left = "";
  String right = "";
  Tuple(this.left, this.right);
  Map<String, String> toJson() => {
    "_object": "Tuple",
    "left": left,
    "right": right
  };
}
Enter fullscreen mode Exit fullscreen mode

The JSON encoder needs a way to know how to convert this new object, it can be done by creating a simple function like the following one. The idea is to check the type of the object passed as argument and to return another object compatible with the JSON format.

Function(dynamic) toEncodable() {
  return (o) {
    switch (o) {
      case Tuple(): return o.toJson();
      case Object(): return o.toString();
    }
  };
}
Enter fullscreen mode Exit fullscreen mode

The toEncodable() function can be passed to the JsonCodec constructor via the toEncodable attribute. The rest is pure testing.

void main() {
  var encoder = JsonCodec(toEncodable: toEncodable());
  var encoded = encoder.encode([
    1,
    "test",
    Object(),
    Tuple("Left1", "Right1")
  ]);
  print(encoded);
}
Enter fullscreen mode Exit fullscreen mode

Let execute the code and check the result.

$ dart bin/encode_encable.dart
[1,"test","Instance of 'Object'",{"_object":"Tuple","left":"Left1","right":"Right1"}]
Enter fullscreen mode Exit fullscreen mode

Our object is correctly converted to a JSON map containing a left and right field. Regarding the raw object, we have now a string containing "Instance of 'Object'", that's the direct result of the toString() method.

Last trick, it is also possible to encode a JSON using indentation via the JsonEncoder.withIndent() constructor.

import 'dart:convert';

void main() {
  print(JsonEncoder
    .withIndent(" ")
    .convert(
      {"test": [1,2,3]}
    )
  );
}
Enter fullscreen mode Exit fullscreen mode

Let execute our code, the output should be a JSON with indentation.

$ dart bin/encode_indent.dart
{
 "test": [
  1,
  2,
  3
 ]
}
Enter fullscreen mode Exit fullscreen mode

That's right! It works! Next step, decoding JSON.

Decoding

Decoding a JSON String is similar than encoding Dart objects, one can use JsonCodec or create a JsonDecoder object, the result will be the same.

void main() {
  print(
    json.decode(
      '{"test": 1}'
    )
  );

  print(
    JsonDecoder()
    .convert(
      '{"test": 1}'
    )
  );
}
Enter fullscreen mode Exit fullscreen mode

Let execute this code.

$ dart run bin/decode.dart
{test: 1}
{test: 1}
Enter fullscreen mode Exit fullscreen mode

Without surprise, it returns a Map(), but, what if we want to return a specific Object instead? Let take our Tuple class back from the Encoding section.

class Tuple {
  String left = "";
  String right = "";
  Tuple(this.left, this.right);
  Map<String, String> toJson() => {
    "_object": "Tuple",
    "left": left,
    "right": right
  };
}
Enter fullscreen mode Exit fullscreen mode

The JsonDecoder class can accept a reviver() function passed as parameter and can be used for this kind of situation. When some data looks like it's a specific object, the reviver() function will be able to return the correct instantiated object. Here an example:

Object? Function (Object? key, Object? value)? reviver() {
  return (key, value) {
    print("--> key: $key");
    print("--> value: $value");
    switch (value) {
      case {"_object": "tuple"}: 
        var v = value as Map<String,dynamic>;
        return Tuple(v["left"] ?? "", v["right"] ?? "");
      default: return value;
    }
  };
}
Enter fullscreen mode Exit fullscreen mode

The snippet above will check each value returned by the decoder, and if one of these values are matching a specific kind of pattern (in our case if the value is a Map containing a field object containing the string tuple), then it will create the object. Let check that with a new main entry-point.

void main() {
  var toEncode = {
    "1": [{ 
      "test": {
        "data": 2
      }
    }]
  };
  var decoder = JsonDecoder(reviver());
  var x = decoder.convert(
    json.encode(
      toEncode
    )
  );
  print("==> final: $x");
}
Enter fullscreen mode Exit fullscreen mode

Now, we can execute the code and check the output.

$ dart bin/decode_reviver.dart
--> key: data
--> value: 2
--> key: _object
--> value: tuple
--> key: left
--> value: left
--> key: right
--> value: right
--> key: tuple
--> value: {_object: tuple, left: left, right: right}
--> key: test
--> value: {data: 2, tuple: Instance of 'Tuple'}
--> key: 0
--> value: {test: {data: 2, tuple: Instance of 'Tuple'}}
--> key: 1
--> value: [{test: {data: 2, tuple: Instance of 'Tuple'}}]
--> key: null
--> value: {1: [{test: {data: 2, tuple: Instance of 'Tuple'}}]}
==> final: {1: [{test: {data: 2, tuple: Instance of 'Tuple'}}]}
Enter fullscreen mode Exit fullscreen mode

As you can see, the returned value by the decoder includes an instance of a Tuple class. The code is still a bit messy, not enough check, but the idea is there.

Conclusion

This post was a small one, JSON is a simple format, and the interfaces used to encode/decode it are relatively easy to use, at least, for simple use cases. JSON can also be specified using schema or used as serializer, but it will be seen in another publication. As usual, for the ones would like to dive deeper in the subject, here few interesting links for you.

Have fun!


Cover Image by Dieter K on Unsplash

Top comments (0)