Writing ASDF extensions

Supporting new types in asdf is easy. There are three pieces needed:

  1. A YAML Schema file for each new type.
  2. A Python class (inheriting from asdf.AsdfType) for each new type.
  3. A Python class to define an “extension” to ASDF, which is a set of related types. This class must implement the asdf.AsdfExtension abstract base class.

For an example, we will make a type to handle rational numbers called fraction.

First, the YAML Schema, defining the type as a pair of integers:

%YAML 1.1
---
$schema: "http://stsci.edu/schemas/yaml-schema/draft-01"
id: "http://nowhere.org/schemas/custom/1.0.0/fraction"
title: An example custom type for handling fractions

tag: "tag:nowhere.org:custom/1.0.0/fraction"
type: array
items:
  type: integer
minItems: 2
maxItems: 2
...

Then, the Python implementation. See the asdf.AsdfType and asdf.AsdfExtension documentation for more information:

import os

import asdf
from asdf import util

import fractions

class FractionType(asdf.AsdfType):
    name = 'fraction'
    organization = 'nowhere.org'
    version = (1, 0, 0)
    standard = 'custom'
    types = [fractions.Fraction]

    @classmethod
    def to_tree(cls, node, ctx):
        return [node.numerator, node.denominator]

    @classmethod
    def from_tree(cls, tree, ctx):
        return fractions.Fraction(tree[0], tree[1])

class FractionExtension(object):
    @property
    def types(self):
        return [FractionType]

    @property
    def tag_mapping(self):
        return [('tag:nowhere.org:custom',
                 'http://nowhere.org/schemas/custom{tag_suffix}')]

    @property
    def url_mapping(self):
        return [('http://nowhere.org/schemas/custom/1.0.0/',
                 util.filepath_to_url(os.path.dirname(__file__))
                 + '/{url_suffix}.yaml')]

Adding custom validators

A new type may also add new validation keywords to the schema language. This can be used to impose type-specific restrictions on the values in an ASDF file. This feature is used internally so a schema can specify the required datatype of an array.

To support custom validation keywords, set the validators member of an AsdfType subclass to a dictionary where the keys are the validation keyword name and the values are validation functions. The validation functions are of the same form as the validation functions in the underlying jsonschema library, and are passed the following arguments:

  • validator: A jsonschema.Validator instance.
  • value: The value of the schema keyword.
  • instance: The instance to validate. This will be made up of basic datatypes as represented in the YAML file (list, dict, number, strings), and not include any object types.
  • schema: The entire schema that applies to instance. Useful to get other related schema keywords.

The validation function should either return None if the instance is valid or yield one or more asdf.ValidationError objects if the instance is invalid.

To continue the example from above, for the FractionType say we want to add a validation keyword “simplified” that, when true, asserts that the corresponding fraction is in simplified form:

from asdf import ValidationError

def validate_simplified(validator, simplified, instance, schema):
    if simplified:
        reduced = fraction.Fraction(instance[0], instance[1])
        if (reduced.numerator != instance[0] or
            reduced.denominator != instance[1]):
            yield ValidationError("Fraction is not in simplified form.")

FractionType.validators = {'simplified': validate_simplified}