API Changes in JSV 0.19

Copy Markdown View Source

JSV 0.19 overhauls the cast system to support multiple casts per schema and parameterized cast functions. The new system uses the x-jsv-cast extension keyword and the xcast helper, replacing the previous jsv-cast / with_cast API.

For the full cast guide, see Custom Cast Functions.

New keyword: x-jsv-cast

The cast data is now stored under the x-jsv-cast JSON Schema extension keyword instead of jsv-cast.

The value is a list of casters. Each caster is a string (module name) or a list [module_string, tag, ...extra_args]:

{
  "type": "string",
  "x-jsv-cast": [["Elixir.MyApp.Cast", "to_uppercase"]]
}

Multiple casts on the same schema are applied in order. If any cast fails, the chain stops.

New helper: xcast/2

Use JSV.Schema.xcast/2 instead of JSV.Schema.with_cast/2 to add casts to a schema:

# Before
schema = JSV.Schema.Helpers.string() |> JSV.Schema.with_cast(MyApp.Cast.to_uppercase())

# After
schema = JSV.Schema.Helpers.string() |> JSV.Schema.xcast(MyApp.Cast.to_uppercase())

xcast/2 supports chaining for multiple casts:

schema =
  JSV.Schema.Helpers.string()
  |> JSV.Schema.xcast(MyApp.Cast.to_uppercase())
  |> JSV.Schema.xcast(MyApp.Cast.append_suffix(["!"]))

Cast functions with arguments

Cast handlers can now receive extra arguments. Define the function with arity 2 (data, args) or arity 3 (data, args, vctx):

defcast append_suffix(data, args) do
  [suffix] = args
  {:ok, data <> suffix}
end

The generated helper takes a list of extra arguments:

MyApp.Cast.append_suffix(["!"])
# => ["Elixir.MyApp.Cast", "append_suffix", "!"]

Arguments must be JSON-encodable data. This design prevents schema injection issues.

Breaking changes

defcast :atom requires def, not defp

When using the atom form to refer to an existing local function, that function must now be public (def). defp functions are no longer supported because arity discovery happens at build time via function_exported?/3.

# Before (no longer works)
defcast :to_upper
defp to_upper(data), do: {:ok, String.upcase(data)}

# After
defcast :to_upper
def to_upper(data), do: {:ok, String.upcase(data)}

format_error receives args instead of tag

The format_error/3 callback now receives the full arguments list (including the tag) as its first argument, instead of just the tag:

# The first argument is now the full args list, which starts with the tag for casts defined with `defcast`.
def format_error(["safe_to_atom"], :unknown_atom, data) do
  "could not cast to existing atom: #{inspect(data)}"
end

Unresolved casts fail at build time

Casts that cannot be resolved (missing module, missing __jsv__/2 callback, no matching function at any supported arity) now fail at build time with a clear error. Previously, some of these errors would only surface at validation time.

Both keywords on the same schema is an error

A schema that contains both jsv-cast and x-jsv-cast will fail to build. Migrate each schema individually.

__jsv__/1 callback is now __jsv__/2

The internal cast resolution callback now receives the builder as a second argument and must return the builder along with its result. This allows casters to accumulate build warnings and to opt out of being installed.

The cast tuple also carries the raw schema being built as a third element, so casters can inspect the parent schema (its declared properties, type, etc.) at build time.

# Before
def __jsv__({:cast, ["some_tag" | rest_args]}) do
  {__MODULE__, :do_cast, 1, rest_args}
end

# After
def __jsv__({:cast, ["some_tag" | rest_args], _raw_schema}, builder) do
  {{__MODULE__, :do_cast, 1, rest_args}, builder}
end

A caster can also return {:nocast, builder} to disable itself for the current build, which is what the new :atoms option uses to drop the atom-creating casts when atoms are not allowed.

__jsv__/1 is no longer recognized. Users that defined __jsv__/1q manually must update their signature. Users that rely on defcast get the new signature for free.

Dropped support for Poison

JSV no longer supports the Poison JSON library. Security concerns required upgrading the Decimal dependency to 3.0, and that version is incompatible with Poison.

If your project depends on Poison as its JSON library, switch to Jason or the JSON module from Elixir's standard library before upgrading JSV.

User impact should be minimal: JSV uses a JSON parser only when resolving remote schemas through the built-in JSV.Resolver.Httpc resolver. All other JSV operations work on pre-parsed data and schemas, so most applications are unaffected by this change.

New build options

:atoms

Controls whether casts that create atoms are allowed. Affects the built-in string_to_atom, string_to_atom_or_nil, string_enum_to_atom and string_enum_to_atom_or_nil casters.

JSV.build!(schema, atoms: true)   # atom casts are installed
JSV.build!(schema, atoms: false)  # atom casts are silently dropped

When the option is not set, atom casts are still installed (for backwards compatibility) but a warning is emitted at build time. The default will flip to false in v2. Set the option explicitly to silence the warning and lock in the behavior you want.

Use false for schemas built from untrusted input at runtime, to prevent third parties from causing arbitrary atom creation through x-jsv-cast.

:warnings

Controls how build-time warnings are surfaced.

  • :emit (default) - warnings are printed via IO.warn/2.
  • :silent - warnings are not printed.

In both cases, all warnings emitted during the build are returned on the JSV.Root struct under the warnings field, with their key, message and schema path:

root = JSV.build!(schema, warnings: :silent)
root.warnings
# => [%{key: :unsafe_atoms, message: "...", rev_path: [...]}]

Because the default is :emit, existing callers will start to see IO.warn/2 output the first time they build a schema that uses an atom caster without setting :atoms. Either pass atoms: true | false explicitly to silence the deprecation warning, or pass warnings: :silent to suppress build warnings entirely.

Legacy jsv-cast support

The legacy jsv-cast keyword is still honored. Existing schemas that use it will continue to work without changes during the deprecation period. You can migrate at your own pace.

defschema modules now emit x-jsv-cast. The with_cast/2 helper still emits jsv-cast and will be deprecated in a future release.

Migration checklist

  1. Replace JSV.Schema.with_cast(...) calls with JSV.Schema.xcast(...).
  2. If you wrote jsv-cast values in raw schema maps, switch the key to x-jsv-cast and wrap the caster in a list (it is now a list of casters).
  3. Update any defcast :atom targets from defp to def.
  4. Review format_error/3. The first argument is now the full args list.
  5. Run your tests. Existing defschema structs keep working as-is.