Skip to content

Narazaka/json-schema-serializer

Repository files navigation

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.