No commit activity in last 3 years
No release in over 3 years
JSON Schema based serializer
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
2024
2025
 Dependencies

Development

 Project Readme

JSON::Schema::Serializer

Actions Status Gem Version

JSON Schema based serializer

Installation

Add this line to your application's Gemfile:

gem 'json-schema-serializer'

And then execute:

$ bundle

Or install it yourself as:

$ gem install json-schema-serializer

Usage

require "json/schema/serializer"

schema = {
  type: "object",
  properties: {
    id: { type: "integer" },
    name: { type: "string" },
    fuzzy: { type: ["string", "integer", "null"] },
  },
  required: ["id"],
}

serializer = JSON::Schema::Serializer.new(schema)

serializer.serialize({id: "42", name: "me", foo: "bar", fuzzy: "1000"})
# => {"id"=>42, "name"=>"me", "fuzzy"=>"1000"}
# "42" -> 42! type coerced!

serializer.serialize({id: "42", name: "me", fuzzy: 42})
# => {"id"=>42, "name"=>"me", "fuzzy"=>42}
serializer.serialize({id: "42", name: "me"})
# => {"id"=>42, "name"=>"me", "fuzzy"=>nil}
# multiple type auto select!

serializer.serialize({})
# => {"id"=>0, "name"=>nil, "fuzzy"=>nil}
# nil -> 0! required property's type coerced!

serializer.serialize({id: 10, name: "I don't need null keys!"}).compact
# => {"id"=>10, "name"=>"I don't need null keys!"}
# compact it!

class A
  def id
    42
  end
end
serializer.serialize(A.new)
# => {"id"=>42, "name"=>nil, "fuzzy"=>nil}
# method also allowed

class Schema
  def type
    :string
  end
end
serializer2 = JSON::Schema::Serializer.new(Schema.new)
serializer2.serialize(32)
# => "32"
# non-hash schema allowed

#
# object injector allowed!
#

class FooSerializer
  def initialize(model)
    @model = model
  end

  def first
    @model.first
  end

  def count
    @model.size
  end
end

serializer_injected = JSON::Schema::Serializer.new(
  {
    type: :object,
    inject: :Foo,
    properties: {
      first: { type: :integer },
      count: { type: :integer },
    },
  },
  {
    inject_key: :inject,
    injectors: {
      Foo: FooSerializer,
    },
  },
)

serializer_injected.serialize([1, 2, 3])
# => {"first"=>1, "count"=>3}

#
# object injector with context
#

class BarSerializer
  def initialize(model, context = nil)
    @model = model
    @context = context
  end

  def id
    @model[:id]
  end

  def score
    @context[@model[:id]]
  end
end

inject_context = {
  1 => 100,
  2 => 200,
}

serializer_injected_with_context = JSON::Schema::Serializer.new(
  {
    type: :object,
    inject: :Bar,
    properties: {
      id: { type: :integer },
      score: { type: :integer },
    },
  },
  {
    inject_key: :inject,
    injectors: {
      Bar: BarSerializer,
    },
    inject_context: inject_context,
  },
)

serializer_injected_with_context.serialize({ id: 1 })
# => { "id" => 1, "score" => 100 }

#
# inject in serializer
#

class ParentSerializer
  include JSON::Schema::Serializer::WithContext

  def initialize(model, context = nil)
    @model = model
    @context = context
  end

  def id
    @model[:id]
  end

  def score
    @context[:parent_scores][@model[:id]]
  end

  def child
    # it can be
    # with_context(context) { data }
    # with_context(data, context)
    # with_context(data: data, context: context)
    with_context(@context.merge(child_scores: { 1 => 100, 2 => 200 })) do
      @model[:child]
    end
  end
end

class ChildSerializer
  def initialize(model, context = nil)
    @model = model
    @context = context
  end

  def id
    @model[:id]
  end

  def score
    @context[:child_scores][@model[:id]]
  end
end

serializer_injected_with_context_in_serializer = JSON::Schema::Serializer.new(
  {
    type: :object,
    inject: :Parent,
    properties: {
      id: { type: :integer },
      score: { type: :integer },
      child: {
        type: :object,
        inject: :Child,
        properties: {
          id: { type: :integer },
          score: { type: :integer },
        },
      },
    },
  },
  {
    inject_key: :inject,
    injectors: {
      Parent: ParentSerializer,
      Child: ChildSerializer,
    },
    inject_context: { 1 => 10, 2 => 20 },
  },
)

serializer_injected_with_context_in_serializer.serialize({ id: 1, child: { id: 2 } })
# => { "id" => 1, "score" => 10, "child" => { "id" => 2, "score" => 200 } }

#
# also you can inject context with arraylike data
#

class ItemsSerializer
  include JSON::Schema::Serializer::WithContext

  def initialize(models, context = nil)
    @models = models
    @context = context
  end

  def map(&block)
    context = (@context || {}).merge(scores: {...})
    @models.map { |model| block.call(with_context(model, context)) }
    # CAUTION!
    # not like below!
    # with_context(@models.map(&block), context)
    # with_context(context) { @models.map(&block) }
  end
end

#
# inject model can initialize by keywords
#

class KeywordSerializer
  def initialize(data:, context: nil)
    @data = data
    @context = context
  end

  ...
end

serializer_with_keyword_init_inject = JSON::Schema::Serializer.new(
  {
    type: :object,
    inject: :Keyword,
    properties: { ... },
  },
  {
    inject_key: :inject,
    injectors: {
      Keyword: KeywordSerializer,
      Child: ChildSerializer,
    },
    inject_by_keyword: true, # <- keyword_init!
  },
)

"additionalProperties"

"additionalProperties" is allowed but must be a schema object or false. (not true)

If "additionalProperties" does not exists, this serializer works as { additionalProperties": false }.

$ref resolving

JSON::Schema::Serializer does not resolve $ref so use external resolver.

with hana and json_refs gem example:

require "hana"
require "json_refs"
require "json/schema/serializer"

schema = {
  "type" => "object",
  "properties" => {
    "foo" => { "type" => "integer" },
    "bar" => { "$ref" => "#/properties/foo" },
  },
}

serializer = JSON::Schema::Serializer.new(JsonRefs.(schema))
serializer.serialize({foo: 0, bar: "42"})
# => {"foo"=>0, "bar"=>42}

# resolver option also available

def walk(all, part)
  if part.is_a?(Array)
    part.map { |item| walk(all, item) }
  elsif part.is_a?(Hash)
    ref = part["$ref"] || part[:"$ref"]
    if ref
      Hana::Pointer.new(ref[1..-1]).eval(all)
    else
      part.map { |k, v| [k, walk(all, v)] }.to_h
    end
  else
    part
  end
end

serializer2 = JSON::Schema::Serializer.new(schema["properties"]["bar"], {
  resolver: ->(part_schema) do
    walk(JsonRefs.(schema), part_schema))
  end
})

JSON::Schema::Serializer API

.new(schema, options = nil)

The initializer.

schema [any]

JSON schema object. The serializer tries schema["type"], schema[:type] and schema.type!

options [Hash]

options

options[:resolver] [Proc]

schema object $ref resolver

options[:schema_key_transform_for_input] [Proc]

input key transform

new({
  type: :object,
  properties: {
    userCount: { type: :integer },
  },
}, { schema_key_transform_for_input: ->(name) { name.underscore } }).serialize({ user_count: 1 }) == { "userCount" => 1 }

options[:schema_key_transform_for_output] [Proc]

output key transform

new({
  type: :object,
  properties: {
    userCount: { type: :integer },
  },
}, { schema_key_transform_for_output: ->(name) { name.underscore } }).serialize({ userCount: 1 }) == { "user_count" => 1 }

options[:injectors] [Hashlike<String, Class>, Class], options[:inject_key] [String, Symbol], options[:inject_context] [any], options[:inject_by_keyword] [Boolean]

If schema has inject key, the serializer treats data by injectors[inject_key].new(data) (or injectors.send(inject_key).new(data)).

And if inject_context is present, injectors[inject_key].new(data, inject_context) (or injectors.send(inject_key).new(data, inject_context)).

And if inject_by_keyword is true, new(data, inject_context) will be new(data: data, context: inject_context).

See examples in Usage.

CAUTION: In many case you should define the nil? method in the injector class because Injector always initialized by Injector.new(obj) even if obj == nil.

options[:null_through] [Boolean]

If data is null, always serialize null.

new({ type: :string }, { null_through: true }).serialize(nil) == nil

options[:empty_string_number_coerce_null] [Boolean]

If data == "" in integer or number schema, returns nil.

new({ type: :integer }, { empty_string_number_coerce_null: true }).serialize("") == nil

options[:empty_string_boolean_coerce_null] [Boolean]

If data == "" in boolean schema, returns nil.

new({ type: :boolean }, { empty_string_boolean_coerce_null: true }).serialize("") == nil

options[:false_values] [Enumerable]

If specified, boolean schema treats !false_values.include?(data).

new({ type: :boolean }, { false_values: Set.new([false]) }).serialize(nil) == true

options[:no_boolean_coerce] [Boolean]

If true, boolean schema treats only true to be true.

new({ type: :boolean }, { no_boolean_coerce: true }).serialize(1) == false

options[:guard_primitive_in_structure] [Boolean]

If true, array or object schema does not accept primitive data and returns empty value.

new({ type: :object }, { guard_primitive_in_structure: true }).serialize(1) == {}
new({ type: :object }, { guard_primitive_in_structure: true, null_through: true }).serialize(1) == nil

#serialize(data)

Serialize the object data by the schema.

data [any]

Serialize target object. The serializer tries data["foo"], data[:foo] and data.foo!

JSON::Schema::Serializer::WithContext API

#with_context!(data, context), #with_context!(data: data, context: context), #with_context!(context) { data }

If you use with_context!(data, context) as the return value of the serializer, then "child" serializer can use that context.

See examples in Usage.

License

Zlib License

Development

After checking out the repo, run bin/setup to install dependencies. Then, run rake spec to run the tests. You can also run bin/console for an interactive prompt that will allow you to experiment.

To install this gem onto your local machine, run bundle exec rake install. To release a new version, update the version number in version.rb, and then run bundle exec rake release, which will create a git tag for the version, push git commits and tags, and push the .gem file to rubygems.org.

Contributing

Bug reports and pull requests are welcome on GitHub at https://github.com/Narazaka/json-schema-serializer.