JSON Schema Tools
Toolbox to work with JSON Schema in Ruby:
- blow up classes from schema
- object.as_schema_json conversion
- object.valid? validations with object.errors
Simply use full blown classes or customize the bits and pieces:
- add schema properties to an existing class
- add validations to an existing class
- clean parameters e.g. in an api controller
- customize json output, object namespaces, used schema
Usage
Hook the gem into your app
gem 'json_schema_tools'
Quickstart assuming you have schema definitions like those in place.
# add schema directory to the search path
SchemaTools.schema_path = '/path/to/json-schema-files'
# blow up classes for each schema file
SchemaTools::KlassFactory.build
# init with params (assumes a contact.json definition)
contact = Contact.new first_name: 'Barry', last_name: 'Cade'
# use setters/getters
contact.first_name = 'Ahr'
# validations derived from property definitions
contact.valid?
contact.errors.full_messages
Schema handling
Before the fun begins, with any of the tools, one or multiple JSON schema(files) must be available. A schema is converted into a ruby hash and for convenience is cached into a registry (global or per reader object). Globals are initialized once e.g on program start. A reader object allows to handle schemata in dynamic ways.
When using the global SchemaTools::Reader simply provide a base path to the schema files and you are done.
SchemaTools.schema_path = '/path/to/schema-json-files'
Read a single schema:
schema = SchemaTools::Reader.read :client
Read a schema from an existing Ruby hash:
schema = SchemaTools::Reader.read :client, { ... }
Read multiple schemas, all *.json files in schema path
schemata = SchemaTools::Reader.read_all
Schemata are cached in registry
SchemaTools::Reader.registry[:client]
Read files from a custom path?
schema = SchemaTools::Reader.read :client, 'my/path/to/json-files'
schemata = SchemaTools::Reader.read_all 'my/path/to/json-files'
Don't like the global path and registry? Create an instance and your save with a local registry. Use if you have schema with same names but different markup!
reader = SchemaTools::Reader.new
reader.read :client, 'from/path'
reader.registry
Get a schema as plain ruby hash with all $refs de-referenced
SchemaTools::Reader.read_all
client_schema = SchemaTools::Reader.registry[:client]
schema_hash = client_schema.to_h
Object to JSON - from Schema
As you probably know such is done e.g in rails via object.as_json. While using this might be simple, it has a damn big drawback: There is no transparent contract about the data-structure, as rails simply uses all fields defined in the database(ActiveRecord model). One side-effect: With each migration you are f***ed
A schema provides a public contract about an object definition. Therefore an internal object is converted to it's public(schema) version on delivery(API access). First the object is converted to a hash containing only the properties(keys) from its schema definition. Afterwards it is a breeze to convert this hash into JSON, with your favorite generator.
Following uses client.json schema, detected from peter.class name.underscore => "client", inside the global schema_path and adds properties to the clients_hash by simply calling client.send('property-name'):
class Client < ActiveRecord::Base
include SchemaTools::Modules::AsSchema
end
peter = Client.new name: 'Peter'
peter.as_schema_json
#=> "client":{"id":12, "name": "Peter", "email":"",..}
peter.as_hash
#=> "client"=>{"id"=>12, "name"=> "Peter", "email"=>"",..}
The AsSchema module is a tiny wrapper for following low level method:
paul = Contact.new name: 'Paul'
contact_hash = SchemaTools::Hash.from_schema(paul)
#=> "contact"=>{"id"=>12, "name"=> "Paul",..}
# to_json is up to you .. or your rails controller
Customise Output JSON / Hash
Following examples show options to customize the resulting json or hash. Of course they can be combined.
Only use some fields e.g. to save bandwidth
peter.as_schema_json(fields:['id', 'name'])
#=> "client":{"id":12, "name": "Peter"}
Use a custom schema name e.g. to represent a client as contact. Assumes you also have a schema named contact.json
peter.as_schema_json(class_name: 'contact')
Set a custom schema path
peter.as_schema_json( path: 'path-to/json-files/')
Use your custom reader (preferred over path usage)
reader = SchemaTools::Reader.new
reader.read_all '/your/custom/path-to-schema-json/
peter.as_schema_json( reader: reader)
By default the object hash has the class name (client) and the link-section on root level. This divides the data from the available methods and makes a clear statement about the object type(it's class). If you don't want to traverse that one extra level you can exclude the root and move the data one level up. See how class name and links are available inline:
peter.as_schema_json( exclude_root: true )
#=> {"id":12, "name"=> "Peter",
# "_class_name":"client",
# "_links":[ .. ] }
Of course the low level hash method also supports all of these options:
client_hash = SchemaTools::Hash.from_schema(peter, fields:['id', 'name'])
#=> "client"=>{"id"=>12, "name"=> "Peter"}
Object from JSON
On any remote site you'll need to instantiate new objects from an incoming json string. Let's assume a contact class which you need for incoming contact json data. (Example taken from attributes_spec.rb)
SchemaTools.schema_path = '/path/to/schema-json-files-with-contact-def'
class Contact
include SchemaTools::Modules::Attributes
has_schema_attrs :contact
end
json_str = '{"id": "123456", "last_name": "Meier","first_name": "Peter"}'
c = Contact.from_json(json_str)
c.id #=> "123456"
# new object from hash (if your favorite http tool already parsed the json)
c = Contact.from_hash({"id"=>123456, "last_name"=>"Meier","first_name"=>"Peter"})
Parameter cleaning
Hate people spamming your api with wrong object fields? Use the Cleaner to check incoming params.
For example in a client controller
def create
SchemaTools::Cleaner.clean_params!(:client, params[:client])
# params[:client] now only has keys defined as writable in client.json schema
#..create and save client
end
Object attributes from Schema
Add methods, defined in schema properties, to an existing class. Very useful if you are building a API client and don't want to manually add methods to you local classes .. like people NOT using JSON schema
class Contact
include SchemaTools::Modules::Attributes
has_schema_attrs :client
end
contact = Client.new
contact.last_name = 'Rambo'
# raw access
contact.schema_attrs
# to json
contact.as_schema_json
Classes from Schema - KlassFactory
Use the KlassFactory to directly create classes, with all attributes from a schema. Instead of adding attributes to an existing class like in above example. The classes are named after each schema's [name] (in global path). So lets assume you have a 'client.json' schema with a name attribute in it, for the following examples:
SchemaTools::KlassFactory.build
client = Client.new first_name: 'Heinz'
client.name = 'Schultz'
client.valid?
client.errors.full_messages
Rather like a namespace? Good idea, but don't forget the class or module must be defined.
module SalesKing; end
SchemaTools::KlassFactory.build namespace: SalesKing
contact = SalesKing::Contact.new
Add a custom schema reader most likely useful in conjunction with a custom path
reader = SchemaTools::Reader.new
SchemaTools::KlassFactory.build reader: reader, path: HappyPdf::Schema.path
$ref
Provides some basic support for JSON Pointer. JSON Pointer expressions must reference local files and must contain a fragment identifier, i.e.
./some_include.json#properties
./some_include.json#
is a resolvable pointer, while
http://example.com/public_schema.json
is not.
Real world examples
- SalesKing json schema
- FidorBank json schema
- HappyPdf json schema
- DocTag ruby gem and DocTag json-schema
- .. Your UseCase here
Test
Only runs on Ruby 1.9+ and by default uses most recent ActiveModel version (>3).
bundle install
rake spec
Testing with different ActiveModel / ActiveSupport Versions:
RAILS_VERSION=3.1 bundle install
rake spec
# or if already installed
RAILS_VERSION=4 rake spec
The RAILS_VERSION switch sets the version of the gems in the Gemfile and is only useful in test env.
Credits
Copyright 2012-2016, Georg Leciejewski, MIT License