Schemas for schemaless data
Switch branches/tags
Nothing to show
Fetching latest commit…
Cannot retrieve the latest commit at this time.
Failed to load latest commit information.

Data bindings

There are many ways to represent data. For instance, XML, JSON and YAML are all very similar while having different representations. Data bindings attempts to unify these various representations by allowing the creation of representation-free schemas which can be used to valiate a document. As well, it provides adapters to normalize access across these various types.

Data bindings has four central concepts. Adapters provide normal access independent of representation. Readers allow you to define adapter-independent ways of reading data. Writers allows you define adapter-independent ways of writing data.Validations allow you to define a schema for your document.

5 minute demo

Start by loading from a JSON object

a = DataBindings.from_json('{"name":"Proust","books":[{"published":1913,"title":"Swan\'s Way"},{"published":1923,"title":"The Prisoner"}]}')

(You could also load from YAML, XML BSON, etc by using #from_yaml, #from_xml, #from_bson and so forth)

We can go ahead and access that like we nomrally would

# "proust"
# "Swan's Way"

Great, now let's get a validated copy of that object

b = a.bind {
  property :name, String
  property :books, [] {
    property :published, Integer
    property :title, String

Is it okay?

# => true

How about we represent it in YAML!

# => "---\nname: Proust\nbooks:\n- published: 1913\n  title: Swan's Way\n- published: 1923\n  title: The Prisoner\n" 

Or, right out to a YAML file


And load it back

from_file = DataBindings.from_yaml_file("/tmp/proust.yaml")
from_file.bind(a) # Use the binding from above

We can also define the types independently so that we can associate them with Ruby constructors later.

DataBindings.type(:book) {
  property :published, Integer
  property :title, String

DataBindings.type(:person) {
  property :name
  property :books, [:book]

proust = DataBindings.from_yaml_file('/tmp/proust.yaml').bind(:person)
p proust[:name]
# => "Proust" 
p proust[:books][1]
# => {"published"=>1923, "title"=>"The Prisoner"}

Maybe we also want to create a Ruby object out of person, let's do that.

class Person
  attr_reader :name, :books

  def initialize(name, books)
    @name = name
    @books = books

  def proust?
    name.downcase == 'proust'

class Book
  attr_reader :published, :title

  def initialize(published, title)
    @published, @title = published, title

  def published_before?(year)
    published < year

DataBindings.for_native(:person) { |attrs|[:name], attrs[:books]) }
DataBindings.for_native(:book) { |attrs|[:published], attrs[:title]) }

proust = DataBindings.from_yaml_file('/tmp/proust.yaml').bind!(:person).to_native
# => true
# => true
# => false


Adapters have a simple contract. They must be a module. They must define a method #from_* where * is a type. For example, the JSONAdapter provides #from_json. They must also provide a singleton method #construct that can serialize an object into it's target representation. They may provide other methods to your base generator; they are included into it and thus can access any of it's internals. They are typically expected to return a ruby hash or array. For instance:

a = DataBindings.from_json('{"Hello":"World"}')
# => {"Hello"=>"World"} 
# => DataBindings::Adapters::Ruby::RubyObjectAdapter 


Bindings provide a mechanism to validate certain properties of a Hash.

To create a type, define it from your generator. For example:

DataBindings.type(:person) do
  property :name, String
  property :age, Integer

Would define a type for :person. This object would have two properties name and age. The types available are String, Integer, Float, DataBindings::Boolean. As well, you can refer to any of the types you've defined previously. You can refer to an implicit array of values by putting the type in []. For example, you could have

DataBindings.type(:person) do
  property :name, String
  property :age, Integer
  property :lottery_numbers, [Integer]


Readers provide an adapter-indepedent way of reading data from other sources. By default, we are also dealing with a String representation of the data. For instance:


would create a JSON representation. You could provide file access by adding a file reader.

DataBindings.reader(:file) { |f| }

Now, we could load the above JSON from disk by using


The #from_json_file method is synthesized into your generator by adding a :file reader. By default, there are readers for files, io, and http.


Writers provide an adapter-indepedent way of writing data to other sources. By default, we emit our representation of the data as a String. For instance:

DataBindings.from_ruby({"Hello" => "World"}).convert_to_yaml

would create a YAML representation. You could provide file writing by adding a file writer.

DataBindings.reader(:file) { |obj, f|, 'w') { |h| h << obj } }

Now, if you wanted to write the above JSON to disk as YAML, you could do the following:

DataBindings.from_ruby({"Hello" => "World"}).convert_to_file(:yaml, "/tmp/out.yaml")

The #convert_to_file method that would be synthesized into your generator. By default, there are writers for files, io, and http.