Ideas draft

June 14, 2026 · View on GitHub

v11

ideas

  • Add promise type and S.promise (instead of async flag internally)

TODO:

Test null<> in ppx

// Test that refinement works correctly with reverse

S.reverse(S.schema({
  foo: S.string->S.to(S.number)
})->S.refine(value => value.foo > 0))

TS operation functions

  • rename serializer to reverse parser ?

  • Make foo->S.to(S.unknown) stricter ??

  • Add S.to(from, target, parser, serializer) instead of S.transform?

  • Make built-in refinements not work with unknown. Use S.to (manually & automatically) to deside the type first

  • Better inline empty recursive schema operations (union convert)

  • Don't iterate over JSON value when it's S.json convert without parsing

  • Add S.date.with(S.migrationFrom, S.string, <optionalParser>).

  • Allow to pass {} instead of S.schema({}) to S.array and other schemas

Final release fixes

  • Add S.env to support coercion for union items separately. Like rescript-envsafe used to do with preprocess
  • Make S.record accept two args
  • Update docs

Known bugs left over from the validation refactor (val.validation: array<validationCheck>)

  • Union discriminant hoists refinement checks with && instead of ;. Now that refinements are structured checks, the union item merge loop hoists all checks on a val via andJoinChecks, fusing type checks and refinement checks into one &&-joined condition with a single error throw. This causes two problems: (1) typeof==="string"&&length===N shares one error instead of separate type/refinement errors, and (2) same-type items with different refinements (e.g. S.union([S.string->S.email, S.string->S.url])) lose per-item error messages. Fix: split hoisted checks by fail reference — first group (type checks) → discriminant condition, remaining groups (refinement checks) → body code as cond||fail;. For same-type items with different refinements, use if/else if dispatch on the refinement cond instead of try/catch. Failing regression tests in S_union_test.res.
  • noValidation on a literal inside a union silently breaks dispatch. literalDecoder short-circuits when expectedSchema.noValidation is set and emits no check at all, so there's nothing for the union discriminant hoister to lift — that case becomes a catch-all. Fix: either emit the equality check regardless of noValidation when the val ends up inside a union, or reject S.noValidation on a literal-in-union at schema construction time. Failing regression test: S_noValidation_test.res › Union dispatch still works when a case has noValidation.
  • err.received is wrong for refine-chain vals on type failures. Because B.refine sets ~schema=prev.expected, val.schema on a refined val equals the target schema, and failInvalidType reads val.schema for received. So err.received === err.expected on a primitive type failure. User-visible reason text is unaffected (it uses input->stringify) but programmatic consumers reading err.received get the target schema instead of the source type. Fix: either have the fail function reach through val.prev.schema (with a comment on the invariant that validation-owning vals always have a prev) or stop mutating val.schema to the target in refine and walk the chain differently for "Expected X" messages. FIXME is tagged at Sury.res:failInvalidType.

v11 initial

  • Add s.parseChild to EffectContext ???
  • Support arrays for S.to
  • Remove fieldOr in favor of optionOr?
  • Allow to pass custom error message via .with
  • Make S.to extensible
  • Add S.Date (S.instanceof) and remove S.datetime (S.date added; S.datetime kept for backward compat)
  • Add refinement info to the tagged type

v???

  • S.promise: S.t<'value> => S.t<promise<'value>> and S.await: S.t<promise<'value>> => S.t<'value>
  • Remove S.deepStrict and S.deepStrip in favor of S.deep (if it works)
  • Make S.serializeToJsonString super fast
  • Somehow determine whether transformed or not (including shape)
  • Add JSDoc
  • s.optional for object
  • S.transform(s => { s.reverse(input => input) // Or s.asyncReverse(input => Promise.resolve(input)) input => input }) // or asyncTransform // Maybe format ?
  • Clean up Caml_option.some, Js_dict.get
  • Github Action: Add linter checking that the generated files are up to date (?)
  • Support optional fields (can have problems with serializing) (???)
  • S.mutateWith/S.produceWith (aka immer) (???)
  • Add S.function (?) (An alternative for external ???)

let trimContract: S.contract<string => string> = S.contract(s => {
s.fn(s.arg(0, S.string))
}, ~return=S.string)

  • Use internal transform for trim
  • Add schema input to the error ??? What about build errors?
  • async serializing support
  • Add S.promise
  • S.create / S.validate
  • Add S.codegen
  • Rename S.inline to S.toRescriptCode + Codegen type + Codegen schema using type
  • Make error.reason tree-shakeable
  • S.toJSON/S.castToJson ???
  • S.produce
  • S.mutator
  • Check only number of fields for strict object schema when fields are not optional (bad idea since it's not possible to create a good error message, so we still need to have the loop)

Articles

  • Write an article about creating an AI-friendly JS library (how the API design, type overloads like S.is/S.assert accepting both arg orders, and error messages make Sury easy for both humans and LLMs to use)