Rand Stats

JSON::Class

zef:vrurg

NAME

JSON::Class - general purpose JSON de-/serialization for Raku

SYNOPSIS

use JSON::Class:auth<zef:vrurg>;

role Base is json(:!skip-null) {
    has Num:D $.size is required is json(:name<volume>);
}

class Record is json does Base {
    has Int $.count;
    has Str $.description;
}

say Record.new(:count(42), :description("The Answer"), :size(1.2e0)).to-json;

DESCRIPTION

This module is an alternative to the family of JSON::Marshal, JSON::Unmarshal, JSON::Class modules. It tries to get rid of their weak points and shifts the locus of control from class' outers to its inners. In other words, the class itself is responsible for its de-/serialization in first place. Though it's perhaps, the primary difference, it's far from being the only one.

Also, this module tries to follow the standards of LibXML::Class module by adapting them for differing domain of JSON. First of all, they share the same view on the locus of responsibility. Second, they try to implement declarative semantics in first place.

From here on whenever JSON::Class name is used it refers to this module unless otherwise stated or implied by the context.

BASICS

In this section some basic concepts of JSON::Class are explained. If you feel like skipping it then skip over to the QUICK INTRO BY EXAMPLE section below.

Basic Or "Complex" Types

Some protocols of JSON::Class depend on the kind of type involved. Basic types are considered to be simple or trivially marshalled. Currently these are Mu, Any, Bool, or those consuming Numeric, Stringy, Enumeration roles.

A "complex" type is expected to have marshallable attributes.

Marshalling And JSONification

JSON marshalling requires declaring corresponding entities as JSON-ones, or JSONification. Normally this is done by using trait is json with classes, roles, or attributes. This is declarative, or explicit, JSONification.

It is also possible that implicit jsonification can be used when necessary or desirable. Normally JSON::Class attempts to implement it in a user-transparent way. For example:

class Record {...}
class Archive is json {
    has Record:D @.records;
}

my $arch = Archive.new;
... add records to the archive ...
$arch.to-json;

The class Record above would be implicitly JSONified to produce serialization of a single record. But the JSONified version would never pop on the surface. So, when the archive is deserialized @.records will contain instances of the original Record.

Trait

Trait is json is used for nearly every declaration of JSON elements. The only other trait we have is json-wrap, but it is too early to discuss it.

How something is marshalled is mostly defined by the trait's arguments.

Declarant

In a context of attribute-related discussion it is the object where the attribute was declared in.

Config

Marshalling of deep structures often requires sharing of common options, modifying the process. This is implemented by using a configuration object. A configuration belongs to a dynamic context of marshalling.

There is also a global configuration singleton used when no other configuration provided.

JSON Class

A JSON class is one which has is json trait applied. Sometimes, depending on the context and especially when referring to an instance of the class, another term JSON object can be used.

Laziness

Deserialization of big deeply nested structures could be a long process, no matter if we need an element of the structure right away, or it is going to be requested later, or not used at all. JSON::Class attempts to bring some relieve by postponing deserialization for later time.

Lazy Collections

JSON::Class implements two collection types: sequences and dictionaries, namely JSON sequences and JSON dictionaries. The first is a Postional and Iterable, the second is an Associative.

Both types are primarily defined by these two properties:

JSON Sequence

JSON sequence is a class with is json( :sequence(...) ) trait applied.

Term "sequence" is used here for something that is rather an array and, most definitely, not a sequence (Seq) from Raku's point of view. The term was borrowed from LibXML::Class, which implements the same concept, and where it was adopted from XML schema definitions.

JSON Dictionary

JSON dictionary is, in a way, similar to JSON sequence in terms of been a lazily deserialized collection. Dictionary is an Associative implementation produced with is json( :dictionary(...) ).

Explicit Or Implicit Typeobjects

It could be tedious to marshal a class with many attributes. Most use cases assume that entire object would end up mapped into a JSON structure. Therefore JSON::Class defaults to implicit declaration where all public or is built attributes are JSONified wihout needing explicit is json trait. This is the case demoed in the SYNOPSIS.

In explicit mode one has to manually apply the trait to marshallable attributes.

Declaration

As it was stated earlier, is json is the primary and, basically, the only declarator used by the module. The trait accepts various arguments to modify the declaration according to one's needs. For example, an explicit class is to be declared as:

class ComplexOne is json(:!implicit) {...}

Within the class an attribute for marshalling is to be also marked with is json:

has SomeType $.st is required is json(:skip-null, :!lazy);

Naming Convention

With only few exceptions, names of all attributes and methods introduced by JSON::Class distribution are starting with json- prefix. This rule may not be followed by some internal data structures, but is almost 100% true about entities injected into JSONified typeobject as part of their public or private API.

There is a consequence to this rule: implicit JSONification of attributes skips these with json--prefixed names:

Trait Arguments

Common For Classes And Roles

Class-only

Attribute

Parameters set via is json trait for attributes are normally overriding these set by its declarant. Moreover, unless the declarant is using :implicit or :!implicit argument with its is json trait then declaring an attribute as JSON-marshallable turns its typeobject into an explicit one.

There is an exception though: adverbs :skip, :build, and :aliases, used in any combination, but with no other arguments, doesn't affect the status of attribute's declarant.

Collections

A class is declared as a JSON collection with :sequence or :dictionary (also aliased as :dict) adverbs of the is json trait. The adverbs are expected to be a list of declarations where each item of the list either specify a type constraint or a default value. For dictionaries it is possible to type constrain their keys.

A type constraint declaration can be either:

Typeobject modifiers are:

The last one's meaning is explained below in this section.

The :default declaration is similar in semantics to the is default trait: it sets the value to be used when Nil is assigned into a sequence position, or when a non-existing element requested:

class JDict is json(:dict(..., :default(42))) {}

say JDict.new.<not-here-yet>; # 42

Default is not obliged to be a concrete value.

Whenever a collection is declared with multiple types it is equivalent to declaring an array or a hash with a subset matching all the same types:

class JSeq is json(:sequece(Int:D, Str:D, Foo)) {}

be like:

my subset JSeqOf of Mu where Int:D | Str:D | Foo;
my JSeqOF @jseq;

Dictionary Only Declaration

:keyof(...) declaration can only be used with the :dictionary adverb. It's value has the same format as a single type constraint declaration:

class JDict is json( :dictionary( ..., :keyof(Foo:D()) ) )
class JDict is json( :dictionary( ..., :keyof(Foo:D(), :serializer(&foo2json), :deserializer(&json2foo)) ) )

The Multitype Matching Problem

When a collection is declared with two or more value classes that are not basic types a problem of matching JSON object to a class arises. Apparently, JSON itself lacks any means of distinguishing one JSON object from another (kids, let's say "Hello!" to JavaScript's OO!). In other words, having something like:

[
    {"key1": 1, "key2":"a string"},
    {"keyA": 3.1415926, "keyB": "const"}
]

One wouldn't be able to tell what classes each JSON object represents. The only way for us to distinguish is to guess. Say, we have:

class Foo {
    has Int $.key1;
    has Str $.key2;
}
class Bar {
    has Num $.keyA;
    has Str $.keyB;
}

In this case it is obvious that the first JSON object matches Foo, whereas the other one matches Bar.

This is what JSON::Class sequence basically does by default: it compares sets of keys per each class-candidate to the keys of JSON object. So far – so good, until a very rare case pops up where two classes has the same key names. Say:

class Book {
    has Str:D $.id is required;
    has Str:D $.name is requried;
}
class Article {
    has Str:D $.id is required;
    has Str:D $.name is required;
}

The default matching algorithm would throw here with JSON::Class::X::Deserialize::SeqItem or JSON::Class::X::Deserialize::DictItem exception, depending on the collection type, due to the unresolvable ambiguity. What can we do to avoid that?

One can work around the problem by introducing extra layers of data, for example:

class BookOrArticle is json {
    has Book $.book;
    has Article $.article;
}

This approach unreasonably complicates things and not even always possible.

But what if we know that IDs of books and articles are sufficiently different to tell exactly which is is which? For example, book ID could start with ISBN:, whereas article ID starts with a date-based YYYY-MM-DD: prefix? In this case custom matchers can distinguish one JSON object from another:

This approach, in essence, is applicable to dictionaries too.

There are other ways of solving this task that involve custom marshalling where we can manipulate with serializable data or keys to inject clues as to what's the source type of JSON object was.

Alternative Solutions To Matching

A universal matcher for type can also be set using JSON::Class::Config set-helpers method.

Another way is to try overriding json-guess-descriptor method of a container class. In this case it would even be possible to base your guess on value's position is a sequence, or its key in a dictionary. But to do so one is better be familiar with the internals of JSON::Class.

DYNAMIC VARIABLES

QUICK INTRO BY EXAMPLE

This section contains a series of working code examples demonstrating different features of this module. The samples are slightly stripped down to keep focus on their functionality and don't bother you with boilerplate. Their full code can be found under examples/ subdirectory of the distribution.

Explicit/Implicit

Inheritance And Role Consumption

Incorporation

Notice that except for C3, all other classes and roles are not JSONified. Only attributes of C1 and R1 are serialized.

Lazy Deserialization

Lazy Attribute Build

Config Defaults

C1 specify config defaults but they affect the output only when the immediate instance of the class is serialized. Otherwise if a subclass instance is serialized then its defaults are used. This is to preserve the uniformity of serialization.

Sequence

This example is a bit verbose from both code and output perspective. It's point is to demonstrate the difference between an array and a JSON Sequence type. The following aspects a worth paying attention to:

Apparently, this example doesn't cover all of JSON::Class sequence features.

Dictionary

Array Attributes

Using Configuration And Type Mapping

These two subjects are tightly bound to each other, hence single example for both subjects.

Custom Serialziers And Deserializers

There are more than we can fit into this example. We can have individual custom marshalling for hash and array values, and hash keys too. We can specify marshallers for sequence elements too.

Skipping Custom Marshalling

Here we use custom marshallers too, this time for array values. But there is a trick: serializer and deserializer do not modify every third value. To do so they call json-I-cant routine giving up the task to JSON::Class

METHODS

JSONified Class

SEE ALSO

COPYRIGHT

(c) 2023, Vadim Belman vrurg@cpan.org

LICENSE

Artistic License 2.0

See the LICENSE file in this distributio