YAML Schema with Moose

A friend asked me recently about parsing YAML with perl. He needed to impose some additional structure to a set of YAML documents. His initial approach was to define a new grammar for the language, but this was turning out to be non-trivial — due to significant whitespace and other complexities.

I told him about how I had used Moose types together with YAML.pm to achieve the effect of a “schema” for document types defined on top of YAML. But my solution relied on the “!!perl/hash:Bar” syntax to tell the deserializer how to bless the parsed perl data structure. That’s OK for a purely internal document that I was using, but not appropriate for anything that might be more public.

Stackoverflow led me to a cleaner solution that uses Moose’s type coercion. When passing a raw deserialized perl data structure to a Moose constructor, Moose will look for matching coercions when a type constraint is not initially met.

I’ve been looking for a schema language for YAML and JSON for a while. The wikipedia suggests that Kwalify, Rx, and Doctrine can all fulfill that role, but in the absence of consensus about a schema language, I’d prefer to use something that I have more control over.

Here’s an example. Let’s say we have the following yaml file:

---
name: Extreme Foo
id: 10
alias: FooX
bars:
  - id: 1
    name: bar1
  - id: 2
    name: bar2

The obviously implied Moose types that would define Foo and Bar are:

class Bar {
    has 'name' => (isa => 'Str', is => 'ro', required => 1);
    has 'id' => (isa => 'Int', is => 'ro', required => 1);
}

class Foo {
    has 'name' => (isa => 'Str', is => 'ro', required => 1);
    has 'id' => (isa => 'Int', is => 'ro', required => 1);
    has 'alias' => (isa => 'Str', is => 'ro', required => 0);

    has 'bars' => (isa => 'ArrayRef[Bar]',
		   is => 'ro',
		   required => 1,
		   default => sub { [] } );

    method print() {
	print $self->name . "\n"; # etc
    }
}

Unfortunately Moose will complain about the “bars” variable not being of the correct type. To fix this, we set the coerce flag on the “bars” field, so that Moose will know to go looking for coercion during object construction:

    has 'bars' => (isa => 'ArrayOfBars',
		   is => 'ro',
		   coerce => 1, # This tells Moose to look for matching type coercions
		   required => 1,
		   default => sub { [] } );

In this case — because the “Bar” is embedded in the parameterized ArrayRef type — we also need a new type called ArrayOfBars, and a coercion from ArrayRef[HashRef] to ArrayOfBars.

subtype 'ArrayOfBars'
    => as 'ArrayRef[Bar]';

coerce 'ArrayOfBars'
    => from 'ArrayRef[HashRef]'
    => via { [ map { Bar->new($_) } @{$_} ] };

Meaning that we can now do this with a yaml file that does not contain the “!!” syntax:

my $foo = Foo->new(LoadFile('example2.yaml'));
$foo->print();

The complete code is available on github

Share and Enjoy: These icons link to social bookmarking sites where readers can share and discover new web pages.
  • Digg
  • Sphinn
  • del.icio.us
  • Facebook
  • Mixx
  • Google