# Changelog
All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [Unreleased]
## [0.12.1] - 2026-04-24
### Fixed
- `#{}` (empty-map type) in JSON encode/decode now accepts any map input again — additional fields are silently dropped, matching the behaviour of other literal-field maps. The overly strict rejection introduced in 0.12.0 caused valid use-cases (unconstrained map types) to fail unexpectedly.
## [0.12.0] - 2026-04-24
### Added
- Custom codec can now operate on remote types from modules compiled without `debug_info`. Previously this required debug info to be present; now spectra falls back gracefully and the codec handles decoding directly.
### Changed
- **Breaking**: Custom codec callback signatures have changed. All three callbacks (`encode`, `decode`, `schema`) now receive `CallerTypeInfo :: spectra:type_info()` instead of a bare `Module :: atom()`, and a new `TargetTypeRef :: spectra:sp_type_reference()` argument is added before `TargetType`. The arities are: `encode/6`, `decode/6`, `schema/5`. The argument order is `(Format, CallerTypeInfo, TargetTypeRef, TargetType, Data, Config)` for `encode`/`decode` and `(Format, CallerTypeInfo, TargetTypeRef, TargetType, Config)` for `schema`. The separate `Params` argument no longer exists; type-variable bindings are carried on `TargetType` and accessible via `spectra_type:type_args/1`. Update all custom codec modules to the new signatures.
## [0.11.4] - 2026-04-20
### Added
- `spectra_transform`: a compile-time parse transform that embeds type info directly into a module as an exported `__spectra_type_info__/0` function. Opt in per module with `-compile({parse_transform, spectra_transform}).`. This removes the runtime `beam_lib` overhead for modules that use it; the existing fallback remains unchanged, so adoption is gradual.
### Fixed
- `#{}` (empty-map type) in JSON encode/decode now correctly rejects non-empty maps. Previously it silently accepted and discarded all keys, which caused incorrect union disambiguation — for example, a record's JSON object could decode as `#{}` instead of the record.
## [0.11.3] - 2026-04-19
### Fixed
- `spectra_binary_string` and `spectra_string` now use UTF-8 consistently in both directions when converting between strings and binaries. Previously encode used UTF-8 but decode used raw bytes (latin1), so a non-ASCII value did not roundtrip — e.g. encoding `[233]` (`é`) produced `<<195, 169>>`, which decoded back as `[195, 169]`. Invalid UTF-8 input now returns a structured error instead of silently producing garbage.
- `spectra_json_schema` now emits `anyOf` instead of `oneOf` for union types. `oneOf` requires exactly one branch to match, which rejected values in overlapping unions (e.g. `non_neg_integer() | integer()`, where a positive integer matches both branches). `anyOf` matches Erlang's union semantics of "matches at least one branch".
### Changed
- Expanded property-based test coverage for JSON, string, and binary_string roundtrips and schema validation
## [0.11.2] - 2026-04-17
### Changed
- Optimized JSON traversal hot paths for improved performance
### Fixed
- Configured regex module to support Unicode and UCP for pattern constraints
## [0.11.1] - 2026-04-14
### Changed
- Optimized JSON traversal performance in `spectra_json` encoding and decoding
- Improved internal consumed keys handling using map matching and list-based structures
## [0.11.0] - 2026-04-12
### Added
- `module_types_cache` configuration with `local`, `persistent`, and `none` modes for controlling how module type information is cached.
- A shared internal config path so encoding, decoding, schema generation, and OpenAPI generation use the same resolved runtime configuration throughout a call.
### Changed
- **Breaking**: Custom codec callbacks now receive an extra config argument. `encode` and `decode` move from arity 6 to 7, and `schema` moves from arity 5 to 6.
- **Breaking**: The old `use_module_types_cache` boolean option is replaced by `module_types_cache`.
## [0.10.0] - 2026-04-08
### Added
- **Elixir struct defaults on decode**: When decoding JSON into an Elixir struct type, `Module:__struct__/0` is called to retrieve field defaults. Missing JSON fields now use the struct's own default value instead of erroring — matching how Elixir itself handles struct initialisation. A field absent from JSON still errors if its declared type does not allow `nil`/`undefined` and the struct's default for that field is `nil` (i.e. no explicit default was set).
- **`only` field-filtering in `-spectra()` attribute**: New `only => [field1, field2]` key restricts which fields are included during encoding, decoding, and schema generation — similar to Jason's `only` option. Filtering is applied at type-extraction time, so no changes are needed in encode/decode/schema modules. Propagates through union types (e.g. `MyStruct | nil`) but does not follow user-type or remote-type references.
## [0.9.4] - 2026-04-02
### Fixed
- **OpenAPI component generation bypasses codec `schema/5`**: When a codec-backed named type or record was referenced as an OpenAPI component via `endpoints_to_openapi`, the codec's `schema/5` callback was never called — the structural schema was generated instead.
## [0.9.3] - 2026-04-01
### Added
- **`spectra_type:update_meta/2`**: New helper that updates a type's metadata map in one call, reducing the boilerplate of paired `get_meta`/`set_meta` calls.
- **`make lint`**: New lint target using `elp lint --rebar --read-config` (also added to CI).
### Changed
- `#sp_user_type_ref{}` and `#sp_remote_type{}` internal records now cache `arity` directly, eliminating repeated `length/1` calls across all serialization and deserialization modules.
## [0.9.2] - 2026-03-27
### Added
- **`spectra_calendar_codec`**: Built-in codec for `calendar:datetime()` and `calendar:date()`. Serialises to/from ISO 8601 strings (`"YYYY-MM-DDTHH:MM:SS"` and `"YYYY-MM-DD"`). Opt-in via the application environment, same pattern as `spectra_dict_codec`.
## [0.9.1] - 2026-03-26
### Fixed
- String/binary constraints (`min_length`, `max_length`, `pattern`) are now correctly enforced when the type body is a remote type alias that resolves to a string/binary type (e.g. Elixir's `String.t()`). Previously the constraints were silently ignored.
## [0.9.0] - 2026-03-24
### Added
- **`spectra_dict_codec`**: Built-in codec for encoding and decoding `dict:dict()` values. Register it via the app env or `-behaviour(spectra_codec)` like any other codec. Mostly to show that codecs can be implemented for types with arity > 0.
### Changed
- **Breaking**: `spectra_codec` callbacks now receive an additional `SpType :: spectra:sp_type()` argument. `encode` and `decode` are now arity 6; `schema` is now arity 5. Existing codec modules must add this argument to all callback clauses. Use `spectra_type:type_args/1` on `SpType` to access concrete type-variable bindings at the call site.
## [0.8.2] - 2026-03-21
### Changed
- README: added reference tables documenting all valid `-spectra()` attribute keys for types/records and function specs, including the `title` vs `summary` distinction.
## [0.8.1] - 2026-03-21
### Added
- **Exported OpenAPI types**: `spectra_openapi` now exports `endpoint_spec/0`, `endpoint_doc/0`, `response_spec/0`, `parameter_spec/0`, `parameter_input_spec/0`, `http_method/0`, `http_status_code/0`, and `openapi_metadata/0`.
- **`parameter_input_spec/0`**: New type for the map passed to `with_parameter/3`. Distinct from the internal `parameter_spec/0` (which includes `module`) — the function merges `module` in automatically.
### Changed
- README overhauled: simpler introductory example, cleaner API reference, restructured Custom Codecs section, added `binary_string`/`string` format example.
## [0.8.0] - 2026-03-19
### Added
- **Custom codecs**: New `spectra_codec` behaviour with `encode/4`, `decode/4`, and optional `schema/4` callbacks. Register codecs via the application environment (`{spectra, [{codecs, #{...}}]}`) or by declaring `-behaviour(spectra_codec)` on the type's own module.
- **Type parameters**: Types can now carry a `type_parameters` field in their `-spectra()` attribute. The value is passed as the 4th argument to codec callbacks, allowing a single codec module to handle multiple parameterised variants.
- **Auto-populate `description` and `deprecated` from type annotations**: Parameters, request bodies, and response headers that reference a type with a `-spectra()` doc annotation now automatically inherit `description` and `deprecated`. Explicit values on the spec still take precedence.
- **Plain atom type refs in `spectra_openapi`**: `spectra_openapi` functions now accept plain atoms as type references (e.g. `user` instead of `{type, user, 0}`), matching the behaviour of `spectra.erl`.
### Changed
- **Breaking**: `with_request_body/4` fourth argument changed from an opts map (`#{content_type => ..., description => ...}`) to a plain `content_type` binary. Pass `description` via the type's `-spectra()` annotation instead.
### Fixed
- **`type_doc/2` now follows type references**: The internal `type_doc/2` function in `spectra_openapi` previously returned an empty description whenever the resolved type was an `#sp_user_type_ref{}` or `#sp_remote_type{}`. It now follows the reference to the underlying type to retrieve its description. Local annotations on the alias take precedence — the reference is only followed when the alias itself carries no `-spectra` doc.
## [0.7.0] - 2026-03-04
### Added
- **`description` and `deprecated` fields for OpenAPI parameters**: `parameter_spec()` now accepts `description => binary()` and `deprecated => boolean()` fields, propagated into the generated OpenAPI output.
- **`description` for request body specs**: `request_body_spec()` now accepts a `description` field.
- **`deprecated` for response header specs**: `header_spec()` now accepts a `deprecated => boolean()` field.
- **Extended `openapi_metadata()`**: Supports additional `info` fields and a top-level `servers` list for multi-server OpenAPI documents.
### Changed
- **Breaking**: `with_request_body/3,4` now takes the schema as the third positional argument. Optional metadata (`content_type`, `description`) is passed as a fourth `Opts` map. Update calls from `with_request_body(E, Method, #{schema => S})` to `with_request_body(E, Method, S)`.
## [0.6.0] - 2026-03-04
### Added
- **`-spectra` attribute for function specs**: You can now annotate `-spec` declarations with a `-spectra()` attribute to attach metadata to functions. The `function_doc()` type supports `summary`, `description`, and `deprecated` fields (distinct from the `type_doc()` fields used for types and records).
### Fixed
- `#sp_union{}` `types` field was declared with a default value instead of a type annotation, which could cause subtle runtime issues.
## [0.5.1] - 2026-03-02
### Added
- **`pre_decoded` / `pre_encoded` options** for `decode/5` and `encode/5`: pass `[pre_decoded]` to skip JSON parsing when your input is already a decoded term, or `[pre_encoded]` to get back a `json:encode_value()` term instead of `iodata()` from `encode/5`.
- **`spectra_openapi:endpoints_to_openapi/3`**: new overload that accepts encode options (e.g. `[pre_encoded]`) for the generated OpenAPI document.
### Changed
- `__spectra_type_info__/0` calls are now cached in `persistent_term` alongside abstract-code lookups, reducing repeated reflection overhead.
## [0.5.0] - 2026-02-26
### Added
- **JSON schema documentation**: Types can now carry `title` and `description` metadata via the `spectra` attribute, which is propagated into generated JSON Schema and OpenAPI output
- **`__spectra_type_info__/0` protocol**: Modules can now expose its `type_info` by exporting the `__spectra_type_info__/0` function. This is an implementation detail in the library. It will be used to pair documentation with types in Elixir, and can later be used for performance and to handle hot code reloading better.
## [0.4.0] - 2026-01-27
### Changed
- **Breaking**: `spectra:schema/3` now returns schema values directly instead of `{ok, Schema}` or `{error, Reason}` tuples.
## [0.3.2] - 2026-01-25
### Changed
- Improved documentation and clarified OTP 27 requirement
## [0.3.0] - 2026-01-20
### Changed
- Upgraded JSON Schema from draft-07 to 2020-12 specification with proper `$schema` field support
- Upgraded OpenAPI spec generation from 3.0 to 3.1 (which natively uses JSON Schema 2020-12)
- Improved remote type handling in enums and parameterized types
- Simplified error handling implementation in preparation for better error messages
- Enhanced null/optional handling with clearer documentation and dedicated tests for undefined/nil behavior in mandatory vs optional map fields
### Internal
- Refactored type utilities into `spectra_util.erl` with renamed functions for consistency
- New tests for typed map fields, parameterized remote types, and enum remote types
- Property-based testing for JSON encoding/schema/decoding consistency (`test/prop_json_encode_schema_consistency.erl`)
- Python validators for JSON Schema 2020-12 and OpenAPI 3.1 standards compliance
- Development tooling improvements including updated `.tool-versions` and enhanced `Makefile` with release safeguards
- Major code simplification in `spectra_binary_string.erl`, `spectra_json.erl`, and `spectra_string.erl`
- Created `sp_error.erl` module for consolidated error handling
## [0.2.0] - 2025-12-14
### Changed
- **Breaking**: Extra fields in JSON objects are now ignored during deserialization instead of causing a `not_matched_fields` error. This affects map, struct (elixir) and record deserialization.
### Notes
- The old strict validation behavior has been commented out with a TODO to potentially add it back as a configuration option in the future