Skip to content

Custom types and JSON#

AJson is a dynamic tree. Most of the time you'd rather work with your own structures, so aui::json converts them for you:

  • aui::to_json(value) converts a C++ value to AJson;
  • aui::from_json<T>(json) converts AJson to a C++ value of type T. There is also an overload aui::from_json(json, dst) which writes into an existing object.

Both functions rely on the AJsonConv<T> trait. It is implemented for:

  • numbers (int, int64_t, float, double), bool, AString, APath, AColor;
  • containers (everything that is a ranges::range, e.g., AVector) as JSON arrays;
  • AMap<AString, T> as JSON objects;
  • std::pair as a two-element array;
  • AOptional<T>, _<T>, std::shared_ptr<T> (null for an empty AOptional);
  • enums, by name;
  • properties (AProperty), by their underlying value;
  • your structures, once you describe their fields with AJSON_FIELDS;
  • anything else, once you specialize AJsonConv.

The conversion nests, so AVector<AMap<AString, int>> works out of the box.

Structures#

AJSON_FIELDS describes how fields of a structure map to JSON keys. Place it in the global namespace, after the structure definition.

// ORM data class
struct Data2 {
    AVector<int> values;
    int i;

    bool operator==(const Data2& o) const {
        return values == o.values && i == o.i;
    }
};

AJSON_FIELDS(Data2,
        (values, "values")
        (i, "i")
)

Now the structure converts in both directions:

Data2 d = { {1, 2, 3}, 228 };

// serialization
auto jsonObject = aui::to_json(d);
ASSERT_EQ(AJson::toString(jsonObject), R"({"values":[1,2,3],"i":228})");

// deserialization
auto d2 = aui::from_json<Data2>(jsonObject);
ASSERT_EQ(d, d2);

// all fields are required by default
EXPECT_THROW(aui::from_json<Data2>(AJson::fromString(R"({"i":228})") /* no "values" field present */), AJsonException);

If the field name is the same as the key, AJSON_FIELDS_ENTRY(name) saves some typing.

Optional fields#

By default, a missing key makes aui::from_json throw AJsonException. Pass AJsonFieldFlags::OPTIONAL to allow the key to be absent.

struct DataOptional {
    int v1;
    int v2;
};

// clang-format off
AJSON_FIELDS(DataOptional,
             (v1, "v1")
             (v2, "v2", AJsonFieldFlags::OPTIONAL))
// clang-format on

When the key is missing, the field keeps its previous value, so use aui::from_json(json, dst) with a pre-filled dst to provide defaults:

1
2
3
4
5
// should not throw an exception since v2 is optional
DataOptional dst { 1, 2 };
EXPECT_NO_THROW(aui::from_json<DataOptional>(AJson::fromString(R"({"v1":228})"), dst));
EXPECT_EQ(dst.v1, 228); // should have modified presented value
EXPECT_EQ(dst.v2, 2); // but should have kept previous value

Nested structures#

Fields may be of any type that is convertible, including other structures (with their own AJSON_FIELDS) and containers of them. See the example in Custom converters.

Enums#

Enums are converted by name. Declare them with AUI_ENUM_VALUES, which also makes the names available to AEnumerate.

1
2
3
4
5
enum class Direction {
    UP,
    DOWN,
};
AUI_ENUM_VALUES(Direction, Direction::UP, Direction::DOWN)
EXPECT_EQ(AJson::toString(aui::to_json(Direction::DOWN)), R"("DOWN")");
EXPECT_EQ(aui::from_json<Direction>(AJson::fromString(R"("UP")")), Direction::UP);

Custom converters#

When a type needs a representation of its own, specialize AJsonConv<T> with two static functions: toJson and fromJson.

1
2
3
4
struct Point {
    int x;
    int y;
};

Say we want Point to look like [x, y] instead of {"x":..., "y":...}:

template<>
struct AJsonConv<Point> {
    static AJson toJson(const Point& p) {
        return AJson::Array { p.x, p.y };   // {1, 2} -> [1,2]
    }
    static void fromJson(const AJson& json, Point& dst) {
        const auto& array = json.asArray();
        dst = { array.at(0).asInt(), array.at(1).asInt() };
    }
};
1
2
3
4
5
EXPECT_EQ(AJson::toString(aui::to_json(Point { 1, 2 })), "[1,2]");

auto p = aui::from_json<Point>(AJson::fromString("[3,4]"));
EXPECT_EQ(p.x, 3);
EXPECT_EQ(p.y, 4);

Once specialized, Point can be used as a field or as an element of a container, just like built-in types:

1
2
3
4
5
6
7
8
struct Shape {
    AString name;
    AVector<Point> points;
};
AJSON_FIELDS(Shape,
    (name, "name")
    (points, "points")
)
1
2
3
Shape shape { "triangle", { { 0, 0 }, { 1, 0 }, { 0, 1 } } };
auto str = AJson::toString(aui::to_json(shape));
EXPECT_EQ(str, R"({"name":"triangle","points":[[0,0],[1,0],[0,1]]})");

In fromJson, use the as* accessors of AJson; they throw AJsonTypeMismatchException when the JSON has an unexpected shape, which is what you want.

Error messages#

Exceptions thrown during a conversion carry the location of the faulty value, so you don't have to guess which of the 351 messages is broken. Given these structures:

1
2
3
4
5
6
7
8
9
struct PathTestMessage {
    AString content;
};
AJSON_FIELDS(PathTestMessage, (content, "content"))

struct PathTestRoot {
    AVector<PathTestMessage> messages;
};
AJSON_FIELDS(PathTestRoot, (messages, "messages"))

the following code fails with a message that mentions messages[0].content:

// "content" field receives a number instead of a string
auto json = AJson::fromString(R"({"messages":[{"content":1},{"content":2},{"content":3}]})");
try {
    aui::from_json<PathTestRoot>(json);
    FAIL() << "expected AJsonTypeMismatchException";
} catch (const AJsonTypeMismatchException& e) {
    // the message contains the location of the faulty value, i.e., "messages[0].content: not a AString"
    AString msg = e.getMessage();
    EXPECT_TRUE(msg.contains("messages")) << "path missing 'messages': " << msg;
    EXPECT_TRUE(msg.contains("content")) << "path missing 'content': " << msg;
}

The path is collected automatically for AJSON_FIELDS, containers and maps. A custom AJsonConv that delegates to aui::from_json for its parts benefits from it as well.