[ANN] ReScript Schema PPX

ReScript Schema PPX :tada:

ReScript PPX to generate rescript-schema from type.

:brain: It’s 100% opt-in. You can use rescript-schema without ppx.

Acknowledgments

Big thanks to creator of ppx-spice @moondaddi :pray:. I took a lot of inspiration from the source code of the library. It was especially helpful, since the ppx was my first OCaml program I’ve been working on.

Install

npm install rescript-schema-ppx

Then update your rescript.json config:

{
  ...
+ "ppx-flags": ["rescript-schema-ppx/bin"],
}

Basic usage

// 1. Define a type and add @schema attribute
@schema
type rating =
  | @as("G") GeneralAudiences
  | @as("PG") ParentalGuidanceSuggested
  | @as("PG13") ParentalStronglyCautioned
  | @as("R") Restricted
@schema
type film = {
  @as("Id")
  id: float,
  @as("Title")
  title: string,
  @as("Tags")
  tags: @schema(S.option(S.array(S.string))->S.Option.getOr([])) array<string>,
  @as("Rating")
  rating: rating,
  @as("Age")
  deprecatedAgeRestriction: @schema(S.option(S.int)->S.deprecate("Use rating instead")) option<int>,
}

// 2. PPX will generate the code below
let ratingSchema = S.union([
  S.literal(GeneralAudiences),
  S.literal(ParentalGuidanceSuggested),
  S.literal(ParentalStronglyCautioned),
  S.literal(Restricted),
])
let filmSchema = S.object(s => {
  id: s.field("Id", S.float),
  title: s.field("Title", S.string),
  tags: s.fieldOr("Tags", S.array(S.string), []),
  rating: s.field("Rating", ratingSchema),
  deprecatedAgeRestriction: s.field("Age", S.option(S.int)->S.deprecate("Use rating instead")),
})

// 3. Parse data using the schema
// The data is validated and transformed to a convenient format
%raw(`{
  "Id": 1,
  "Title": "My first film",
  "Rating": "R",
  "Age": 17
}`)->S.parseWith(filmSchema)
// Ok({
//   id: 1.,
//   title: "My first film",
//   tags: [],
//   rating: Restricted,
//   deprecatedAgeRestriction: Some(17),
// })

// 4. Transform data back using the same schema
{
  id: 2.,
  tags: ["Loved"],
  title: "Sad & sed",
  rating: ParentalStronglyCautioned,
  deprecatedAgeRestriction: None,
}->S.serializeWith(filmSchema)
// Ok(%raw(`{
//   "Id": 2,
//   "Title": "Sad & sed",
//   "Rating": "PG13",
//   "Tags": ["Loved"],
//   "Age": undefined,
// }`))

// 5. Use schema as a building block for other tools
// For example, create a JSON-schema with rescript-json-schema and use it for OpenAPI generation
let filmJSONSchema = JSONSchema.make(filmSchema)

:brain: Read more about schema usage in the ReScript Schema for ReScript users documentation.

API reference

@schema

Applies to: type declarations, type signatures

Indicates that a schema should be generated for the given type.

@schema(S.t<'value>)

Applies to: type expressions

Specifies custom schema for the type.

17 Likes

Released v6.3 with more PPX attributes :gear:

@s.matches(S.t<'value>)

:warning: It was previously @schema(S.t<'value>), which is now deprecated and will be completely removed in v7.

Applies to: type expressions

Specifies custom schema for the type.

@schema
type t = @s.matches(S.string->S.String.url) string

// Generated by PPX ⬇️
let schema = S.string->S.String.url

@s.null

Applies to: option type expressions

Tells to use S.null for the option schema constructor.

@schema
type t = @s.null option<string>

// Generated by PPX ⬇️
let schema = S.null(S.string)

@s.nullable

Applies to: option type expressions

Tells to use S.nullable for the option schema constructor.

@schema
type t = @s.nullable option<string>

// Generated by PPX ⬇️
let schema = S.nullable(S.string)

@s.default('value)

Applies to: type expressions

Wraps the type expression schema into an option with the provided default value.

@schema
type t = @s.default("Unknown") string

// Generated by PPX ⬇️
let schema = S.option(S.string)->S.Option.getOr("Unknown")

It might be used together with @s.null or @s.nullable:

@schema
type t = @s.nullable @s.default("Unknown") string

// Generated by PPX ⬇️
let schema = S.nullable(S.string)->S.Option.getOr("Unknown")

@s.defaultWith(unit => 'value)

Applies to: type expressions

Wraps the type expression schema into an option with callback to get the default value.

@schema
type t = @s.defaultWith(() => []) array<string>

// Generated by PPX ⬇️
let schema = S.option(S.array(S.string))->S.Option.getOrWith(() => [])

:brain: The same as @s.default it might be used together with @s.null or @s.nullable

@s.describe(string)

Applies to: type expressions

Adds description property to the generated schema.

@schema
type t = @s.describe("A useful bit of text, if you know what to do with it.") string

// Generated by PPX ⬇️
let schema = S.string->S.describe("A useful bit of text, if you know what to do with it.")

This can be useful for documenting a field, for example in a JSON Schema using a library like rescript-json-schema.

@s.deprecate(string)

Applies to: type expressions

Adds deprecation property to the generated schema.

@schema
type t = @s.deprecate("Will be removed in APIv2") string

// Generated by PPX ⬇️
let schema = S.string->S.deprecate("Will be removed in APIv2")

This can be useful for documenting a field, for example in a JSON Schema using a library like rescript-json-schema.

6 Likes