json-schema

Convert JSON serializable classes into a JSON Schema representation
2.1.1 Latest release released

JSON Schema CI

A crystal lang tool for converting JSON serialisable class definitions into the JSON Schema representation.

Output targets JSON Schema 2020-12 (the dialect of OpenAPI 3.1 and MCP). Pass openapi: true (e.g. MyType.json_schema(true)) for the OpenAPI 3.0 dialect: nullable rather than a null type, tuples as a single items schema and boolean exclusive bounds.

Installation

dependencies:
  json-schema:
    github: spider-gazelle/json-schema

Usage

basic type support


require "json-schema"

String.json_schema #=> {type: "string"}

Int32.json_schema #=> {type: "integer", format: "Int32"}

Float32.json_schema #=> {type: "number", format: "Float32"}

Array(String | Int32).json_schema #=> { type: "array", items: { anyOf: [{type: "integer", format: "Int32"}, {type: "string"}] } }

# Works with enums
enum TestEnum
  Option1
  Option2
end

TestEnum.json_schema #=> {type: "string", enum: ["option1", "option2"]}

json serialisable support is included too, with deeply nested objects etc.


require "json-schema"

class MyType
  include JSON::Serializable

  getter string : String
  getter symbol : Symbol?
  getter time : Time
  getter integer : Int32
  getter union_type : String | Int64 | Bool
end

MyType.json_schema

# outputs

{
  type: "object",
  properties: {
    string:     {type: "string"},
    symbol:     {type: "string"},
    time:       {type: "string", format: "date-time"},
    integer:    {type: "integer", format: "Int32"},
    union_type: {anyOf: [{type: "boolean"}, {type: "integer", format: "Int64"}, {type: "string"}]}
  },
  required: ["string", "time", "integer", "union_type"]
}

You can also customize schema output using the @[JSON::Field] annotation


require "json-schema"

class MyType
  include JSON::Serializable

  # The `EpochConverter` here means the JSON value will actually be an integer
  # so to avoid the output being `type: "string", format: "date-time"` you can
  # supply a type override and custom format string.
  @[JSON::Field(converter: Time::EpochConverter, type: "integer", format: "Int64")]
  getter time : Time

  # or if you just want to provide a custom format
  @[JSON::Field(format: "email")]
  getter email : String
end

for anything too confusing it falls back to a generic { type: "object" } however this should only happen in some cases where you've inherited generic objects. e.g. class Me < Hash(String, Int32) (although this case is handled correctly)

Referencing nested types

By default nested types are expanded inline wherever they appear. Pass a JSON::Schema::Definitions as refs and nested JSON::Serializable types and enums are emitted as $refs instead, with each definition built once and collected for you. Self referencing types are supported in this mode.

struct Item
  include JSON::Serializable
  getter content : String
end

struct List
  include JSON::Serializable
  getter items : Array(Item)
  getter primary : Item?
end

refs = JSON::Schema::Definitions.new # $ref prefix defaults to "#/$defs/"
List.json_schema(refs: refs)
# {type: "object", properties: {items: {type: "array", items: {"$ref": "#/$defs/Item"}}, primary: {anyOf: [{"$ref": "#/$defs/Item"}, {type: "null"}]}}, required: ["items"]}

refs.resolve # => {"Item" => {type: "object", properties: {content: {type: "string"}}, required: ["content"]}}

# for an OpenAPI document
refs = JSON::Schema::Definitions.new("#/components/schemas/")
JSON::Schema.introspect(Array(List), openapi: true, refs: refs)

Types are named by type rather than by shape, so two enums with the same members are kept as separate definitions. A type that describes itself with def self.json_schema(openapi : Bool? = nil) is still referenced, and its definition comes from that method. Definition names only use the characters OpenAPI allows in component names, and the conversion is reversible, so two types can never share a name: :: becomes ., generic and union separators become short escapes (Page(Api::User) is Page-oApi.User-c), see JSON::Schema::Definitions.normalise. A block passed to Definitions.new customises the naming, an error is raised if it gives two types the same name. Siblings of a $ref (nullable, description) are ignored by OpenAPI 3.0, so in those cases the reference is wrapped in an allOf (with the definition's type, which OpenAPI 3.0.3 requires alongside nullable).

json-schema:
  github: spider-gazelle/json-schema
  version: ~> 2.1.1
Crystal none

Dependencies 0

Development Dependencies 1

  • ameba
    {'github' => 'veelenga/ameba'}

Dependents 1

Last synced .
search fire star recently