# API Changes in JSV 0.19

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](../schemas/cast-functions.md).


## 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]`:

```json
{
  "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:

```elixir
# 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:

```elixir
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`):

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

The generated helper takes a list of extra arguments:

```elixir
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`.

```elixir
# 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:

```elixir
# 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.

```elixir
# 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__/1`q 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](https://hex.pm/packages/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.

```elixir
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:

```elixir
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.
