Search code examples
jsonperlserializationcode-generationmoose

Converting JSON string to Perl/Moose objects


I have a JSON string, for example the

use JSON::XS qw(decode_json);
say Dumper( decode_json($json) );

will produce:

$VAR1 = {
    'Fname' => 'SomeFname',
    'Lname' => 'SomeLname',
    'Addr' => {
          'Street => 'Somestreet',
          'Zip' => '00000',
    },
};

I'm looking for an easy way how to "convert" JSON string (or perl structure) into Perl/Moose objects, like:

 package My;
 use Moose;
 has 'Fname' => (is => 'rw', isa => 'Str');
 has 'Lname' => (is => 'rw', isa => 'Str');
 has 'Addr' =>  (is => 'rw', isa => 'My::Addr');

and

 package My::Addr;
 use Moose;
 has 'Street' => (is => 'rw', isa => 'Str');
 has 'Zip' => (is => 'rw', isa => 'Str');

The problem has two parts:

  1. defining the Moose classes hierarchy based on JSON string (one time)
  2. initializing the objects instances with real values from the JSON (for every JSON)

I'm not very skilled in Moose, so need some links what to study for this particular problem.

(Moose is BIG - so reading everything in CPAN is sure helpful but it is too much for a start. Therefore I'm looking for learning step-by-step in real world problem - like above).

The main questions are:

  • is possible generate Moose class definitoins (perl source) from data structure? Exists such of CPAN module?
  • when i got the class hierarchy (e.g. I can write them manually if here isn't any helper), what is the easiest way creating (initializating) their instances from JSON?

Solution

  • is possible generate Moose class definitoins (perl source) from data structure? Exists such of CPAN module?

    Yes. However, how you do that will depend heavily upon what you hope to accomplish. The simplest would be to just build attributes in memory based the JSON. For example, you can instantiate metaclass objects at runtime from a JSON file something like this:

    sub infer_class_from_hash {
        my ($input) = @_;
    
        # Makes for ugly class names, but does the job
        my $meta = Moose::Meta::Class->create_anon_class;
    
        for my $key (keys %$input) {
            my $value = $input->{$key};
    
            my $type;
            my $coerce = 0;
    
            # Handle nested objects in the JSON as Moose objects
            if (ref $value eq 'HASH') {
                my $inner_meta = infer_class_from_hash($value);
                $type = $meta->name;
    
                # We provide an automatic HASH -> Class coercion
                $coerce = 1;
            }
    
            # Assume arrays are always of scalars, could be extended to handle objects
            elsif (ref $value eq 'ARRAY') {
                $type = 'ArrayRef',
            }
    
            # Assume anything else is string-ish
            else {
                $type = 'Str',
            }
    
            $meta->add_attribute($key =>
                is => 'rw',
                isa => $type,
                coerce => $coerce,
            );
        }
    
        # Create a coercion that makes instantiating from the JSON tree dead simple
        use Moose::Util::TypeConstraints;
        coerce $meta->name => from 'HashRef' => via { $meta->name->new($_) };
        no Moose::Util::TypeConstraints;
    
        return $meta;
    }
    

    All that does not even begin to get into what you could do. You could apply roles, custom base classes, add methods, etc. However, that's the basics of building a Moose class on the fly.

    If you want to have a code generator that outputs actual Moose classes as files that can be built once and then loaded later, do whatever you want. I'd just write a program that worked similar to above, but outputs a set of .pm files with the automatically generated Moose class definitions in them.

    when i got the class hierarchy (e.g. I can write them manually if here isn't any helper), what is the easiest way creating (initializating) their instances from JSON?

    use JSON qw( from_json );
    my $hash = from_json("...");
    my $meta = infer_class_from_hash($hash);
    
    my $obj = $meta->name->new($hash);
    

    The key here is how we use Moose type coercion to automatically convert the JSON objects read in into Moose class instances. If you have more than one serialization scheme or want to use type constraints in some other way, you could setup a base class for all of these (in the call to create_anon_class) that provided to_json/from_json methods to handle the case of nested objects in the JSON input.