Skip to content

ajevans99/swift-json-schema

Repository files navigation

Swift JSON Schema

CI SPI Versions SPI Platforms Supported Dialects Draft 2020-12 codecov

The Swift JSON Schema library provides a type-safe way to generate and validate JSON schema documents directly in Swift, with deterministic, byte-stable JSON output at every layer — schema emission, validation results, and the underlying JSON value type.

Library targets

The package ships four libraries you can import independently:

Library Use it when you need
OrderedJSON An order-preserving JSON parser, serializer, and value type. Standalone — use it without the validator if you just want stable JSON I/O across processes.
JSONSchema The JSON Schema 2020-12 validator. Re-exports OrderedJSON so types like JSONValue are available unchanged from import JSONSchema.
JSONSchemaBuilder A result-builder DSL plus the @Schemable macro for generating schemas from Swift types. Builds on JSONSchema.
JSONSchemaConversion Type bridges (URL, UUID, Date) for use with JSONSchemaBuilder.

Schema Generation

Use the power of Swift result builders to generate JSON schema documents.

@JSONSchemaBuilder var personSchema: some JSONSchemaComponent {
  JSONObject {
    JSONProperty(key: "firstName") {
      JSONString()
        .description("The person's first name.")
    }

    JSONProperty(key: "lastName") {
      JSONString()
        .description("The person's last name.")
    }

    JSONProperty(key: "age") {
      JSONInteger()
        .description("Age in years which must be equal to or greater than zero.")
        .minimum(0)
    }
  }
  .title("Person")
}
Generated JSON Schema

Schema returned from personSchema.definition() conforms to Codable.

let encoder = JSONEncoder()
encoder.outputFormatting = .prettyPrinted

let schemaData = try! encoder.encode(personSchema.definition())
let string = String(data: schemaData, encoding: .utf8)!
print(string)
{
  "title": "Person",
  "type": "object",
  "properties": {
    "firstName": {
      "type": "string",
      "description": "The person's first name."
    },
    "lastName": {
      "type": "string",
      "description": "The person's last name."
    },
    "age": {
      "description": "Age in years which must be equal to or greater than zero.",
      "type": "integer",
      "minimum": 0
    }
  }
}

Macros

Use the @Schemable macro from JSONSchemaBuilder to automatically generate the result builders.

@Schemable
@ObjectOptions(.additionalProperties { false })
struct Person {
  let firstName: String

  let lastName: String?

  @NumberOptions(.minimum(0), .maximum(120))
  let age: Int

  /// A short bio or summary about the person, shown on their public profile.
  @StringOptions(.maxLength(500))
  let bio: String?
}
Expanded Macro
struct Person {
  let firstName: String

  let lastName: String?

  let age: Int

  /// A short bio or summary about the person, shown on their public profile.
  let bio: String?

  // Auto-generated schema ↴
  static var schema: some JSONSchemaComponent<Person> {
    JSONSchema(Person.init) {
        JSONObject {
            JSONProperty(key: "firstName") {
                JSONString()
            }
            .required()
            JSONProperty(key: "lastName") {
                JSONString()
            }
            JSONProperty(key: "age") {
                JSONInteger()
                .minimum(0)
                .maximum(120)
            }
            .required()
            JSONProperty(key: "bio") {
                JSONString()
                .maxLength(500)
                .description(#"""
                A short bio or summary about the person, shown on their public profile.
                """#)
            }
        }
        .additionalProperties(false)
    }
  }
}
extension Person: Schemable {}

@Schemable can be applied to enums.

@Schemable
enum Status {
  case active
  case inactive
}
Expanded Macro
enum Status {
  case active
  case inactive
  
  static var schema: some JSONSchemaComponent<Status> {
    JSONString()
      .enumValues {
        "active"
        "inactive"
      }
      .compactMap {
        switch $0 {
        case "active":
          return Self.active
        case "inactive":
          return Self.inactive
        default:
            return nil
        }
      }
    }
}
extension Status: Schemable {}

Enums with associated values are also supported using anyOf schema composition. See the JSONSchemaBuilder documentation for more information.

For details on modeling dependencies and other conditional constructs, check the Conditional Validation guide.

Validation

Using the Schema type, you can validate JSON data against a schema.

let schemaString = """
{
  "type": "object",
  "properties": {
    "name": {
      "type": "string",
      "minLength": 1
    }
  }
}
"""
let schema1 = try Schema(instance: schemaString)
let result = try schema1.validate(instance: #"{"name": "Alice"}"#)

Alternatively, you can use the JSONSchemaBuilder builders (or macros) to create a schema and validate instances.

let nameBuilder = JSONObject {
  JSONProperty(key: "name") {
    JSONString()
      .minLength(1)
  }
}
let schema = nameBuilder.defintion()

let instance1: JSONValue = ["name": "Alice"]
let instance2: JSONValue = ["name": ""]

let result1 = schema.validate(instance1)
dump(result1, name: "Instance 1 Validation Result")
let result2 = schema.validate(instance2)
dump(result2, name: "Instance 2 Validation Result")
Instance 1 Validation Result
▿ Instance 1 Validation Result: JSONSchema.ValidationResult
- isValid: true
▿ keywordLocation: #
  - path: 0 elements
▿ instanceLocation: #
  - path: 0 elements
- errors: nil
▿ annotations: Optional([JSONSchema.Annotation<JSONSchema.Keywords.Properties>(keyword: "properties", instanceLocation: #, schemaLocation: #/properties, absoluteSchemaLocation: nil, value: Set(["name"]))])
  ▿ some: 1 element
    ▿ JSONSchema.Annotation<JSONSchema.Keywords.Properties>
      - keyword: "properties"
      ▿ instanceLocation: #
        - path: 0 elements
      ▿ schemaLocation: #/properties
        ▿ path: 1 element
          ▿ JSONSchema.JSONPointer.Component.key
            - key: "properties"
      - absoluteSchemaLocation: nil
      ▿ value: 1 member
        - "name"
Instance 2 Validation Result
- isValid: false
▿ keywordLocation: #
  - path: 0 elements
▿ instanceLocation: #
  - path: 0 elements
▿ errors: Optional([JSONSchema.ValidationError(keyword: "properties", message: "Validation failed for keyword \'properties\'", keywordLocation: #/properties, instanceLocation: #, errors: Optional([JSONSchema.ValidationError(keyword: "minLength", message: "The string length is less than the specified \'minLength\'.", keywordLocation: #/properties/name/minLength, instanceLocation: #/name, errors: nil)]))])
  ▿ some: 1 element
    ▿ JSONSchema.ValidationError
      - keyword: "properties"
      - message: "Validation failed for keyword \'properties\'"
      ▿ keywordLocation: #/properties
        ▿ path: 1 element
          ▿ JSONSchema.JSONPointer.Component.key
            - key: "properties"
      ▿ instanceLocation: #
        - path: 0 elements
      ▿ errors: Optional([JSONSchema.ValidationError(keyword: "minLength", message: "The string length is less than the specified \'minLength\'.", keywordLocation: #/properties/name/minLength, instanceLocation: #/name, errors: nil)])
        ▿ some: 1 element
          ▿ JSONSchema.ValidationError
            - keyword: "minLength"
            - message: "The string length is less than the specified \'minLength\'."
            ▿ keywordLocation: #/properties/name/minLength
              ▿ path: 3 elements
                ▿ JSONSchema.JSONPointer.Component.key
                  - key: "properties"
                ▿ JSONSchema.JSONPointer.Component.key
                  - key: "name"
                ▿ JSONSchema.JSONPointer.Component.key
                  - key: "minLength"
            ▿ instanceLocation: #/name
              ▿ path: 1 element
                ▿ JSONSchema.JSONPointer.Component.key
                  - key: "name"
            - errors: nil
- annotations: nil

Parsing

When using builders or macros, you can also parse JSON instances into Swift types.

@Schemable
enum TemperatureUnit {
  case celsius
  case fahrenheit
}

@Schemable
struct Weather {
  let temperature: Double
  let unit: TemperatureUnit
  let conditions: String
}

let data = """
{
  "temperature": 20,
  "unit": "celsius",
  "conditions": "Sunny"
}
"""
let weather: Parsed<Weather, ParseIssue> = Weather.schema.parse(instance: data)

Optionally, combine parsing and validation in a single step.

let weather: Weather = try Weather.schema.parseAndValidate(instance: data)

Shoutout to the swift-parsing library and the Point-Free Parsing series for the inspiration behind the parsing API and implementation.

Why not Foundation's JSON?

You can use the validator with JSONDecoder / JSONEncoder if you want — JSONValue is Codable. But this package ships its own JSON parser (OrderedJSON) for one reason: declared key order is preserved through the entire parse → validate → serialize pipeline, byte-stably across processes and platforms.

Foundation's parsers don't promise that:

Operation JSONDecoder JSONSerialization OrderedJSON
Object key order preserved on parse ❌ unspecified ✅ iOS 17+/macOS 14+/tvOS 17+/watchOS 10+ ✅ all platforms
Object key order preserved on emit ❌ unspecified ❌ alphabetical with .sortedKeys ✅ insertion order
Round-trip byte-stable ⚠️ recent Apple platforms only

For schema validation specifically, this matters because:

  • Annotation/error output is deterministic — the order of result.annotations and result.errors reflects the validation traversal, which itself follows the instance's declared key order. Snapshot tests against validation output don't flake.
  • Serialized schemas are byte-stable — a schema constructed via JSONSchemaBuilder (or parsed in) emits JSON in dialect-deterministic order, every run. Snapshots, signed payloads, generated artifacts all stay reproducible.
  • Linux parity — Foundation's JSONSerialization only preserves key order on iOS 17+ / macOS 14+ / tvOS 17+ / watchOS 10+; older OS versions and corelibs Foundation (Linux) make no such promise. OrderedJSON works everywhere SwiftPM does.

Use OrderedJSON directly if you don't need the validator:

import OrderedJSON

let value = try JSONValue.parse(data)            // preserves source key order
let bytes = try value.serializedData()           // emits in that same order
let pretty = try value.serialized(options: .pretty)

See issue #149 for the full background on why the determinism work happened.

Example Projects

Explore these companion repositories to see swift-json-schema in action:

Have a project of your own? We'd love to showcase it. Open a PR to add your repository to this list.

Documentation

The full documentation for this library is available through the Swift Package Index.

Installation

You can add the SwiftJSONSchema package to your project using Swift Package Manager (SPM) or Xcode.

Using Swift Package Manager (SPM)

To add SwiftJSONSchema to your project using Swift Package Manager, add the following dependency to your Package.swift file:

dependencies: [
  .package(url: "https://github.com/ajevans99/swift-json-schema", from: "0.2.1")
]

Then, include JSONSchema and/or JSONSchemaBuilder as a dependency for your target:

targets: [
  .target(
    name: "YourTarget",
    dependencies: [
      .product(name: "JSONSchema", package: "swift-json-schema"),
      .product(name: "JSONSchemaBuilder", package: "swift-json-schema"),
    ]
  )
]

Using Xcode

  1. Open your project in Xcode.
  2. Navigate to File > Swift Packages > Add Package Dependency...
  3. Enter the repository URL: https://github.com/ajevans99/swift-json-schema
  4. Follow the prompts to add the package to your project.

Once added, you can import JSONSchema in your Swift files and start using it in your project.

License

This library is released under the MIT license. See LICENSE for details.

About

Generate JSON Schema documents from Swift

Resources

License

Contributing

Stars

Watchers

Forks

Contributors