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}
endThe 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)}"
endUnresolved 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}
endA 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 droppedWhen 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 viaIO.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
- Replace
JSV.Schema.with_cast(...)calls withJSV.Schema.xcast(...). - If you wrote
jsv-castvalues in raw schema maps, switch the key tox-jsv-castand wrap the caster in a list (it is now a list of casters). - Update any
defcast :atomtargets fromdefptodef. - Review
format_error/3. The first argument is now the full args list. - Run your tests. Existing
defschemastructs keep working as-is.