Changelog

View Source

All notable changes to this project will be documented in this file.

The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.

[Unreleased]

[0.17.0] - 2026-10-31

Added

  • spectra:encode/5 accepts {nil_fields, null | omit}. It sets, for that call only, whether a field whose type can be missing (T | undefined, T | nil) and whose value is the missing value is written as null or left out, nested values included. The schema does not change, so only options whose output the schema accepts are allowed: null or omit with the default setting omit_or_null, and the value that equals the setting otherwise. {nil_fields, null} with the setting omit, {nil_fields, omit} with the setting null, and an unknown option value raise badarg (ArgumentError in Elixir). The error does not include the data being encoded.

  • spectra_openapi:with_nil_fields(Webhook, null) marks a webhook whose payload is encoded with {nil_fields, null}. Its payload schema lists every field whose type can be missing in required and allows null, nested types included. The payload type keeps its schema name, unless the same type is also used without the option. Then the webhook gets a second schema with Nullable added to the name. to_openapi/4 raises badarg when the nil_fields setting is omit, or when that second name is already taken. With the setting null, the option changes nothing.

Changed

  • The nil_fields application setting has a new default, omit_or_null. The JSON Schema and OpenAPI schema of a field whose type can be missing now allow null ("type": [T, "null"], or anyOf with {"type": "null"}), and so does additionalProperties of a typed map whose value can be missing. The field stays out of required, and encoding still leaves it out unless the call passes {nil_fields, null}. To keep the old schema, set {spectra, [{nil_fields, omit}]} (config :spectra, :nil_fields, :omit in Elixir). The setting null is unchanged.
  • The Options of spectra_openapi:endpoints_to_openapi/3 and spectra_openapi:to_openapi/4 are typed as spectra:schema_option(), since nil_fields does not apply to the spec.

Fixed

  • A missing value (nil / undefined) outside a field, such as an element of [integer() | nil] or a whole value of type integer() | undefined, is now encoded as JSON null instead of the string "nil". The JSON Schema of such a list item allows null, and so does the schema of a union with nil / undefined, such as a body or OpenAPI component of that type, unless nil_fields is omit. OpenAPI parameters (path, query, header, cookie) and response headers are excluded: their value is never JSON null, and required: false already says they can be left out, so their schema, and a component that only parameters use, is the same as in 0.16.0.

  • A documented OpenAPI parameter or response header no longer repeats its description and deprecated inside its schema; they appear only on the parameter or header.
  • The message in an error's ctx is now always a binary instead of sometimes a charlist (struct mismatch, not_matched_fields, non-binary input without pre_decoded, dict keys, string conversion).

[0.16.0] - 2026-10-30

Added

  • spectra_openapi:response/2 accepts the OpenAPI 3.1 status code ranges <<"1XX">> to <<"5XX">> besides integer status codes.
  • nil_fields application setting ({spectra, [{nil_fields, null}]}, or config :spectra, :nil_fields, :null in Elixir). With null, a field whose type can be missing (T | undefined, T | nil) and whose value is the missing value is encoded as JSON null instead of being left out, and its schema lists it in required and allows null ("type": [T, "null"] for a plain type, anyOf with {"type": "null"} otherwise). Optional => map keys that are absent from the data stay absent and stay out of required. A typed map value that can be missing, such as in #{binary() => binary() | undefined}, is also encoded as null, and additionalProperties allows null. The default, omit, leaves encode output and schemas unchanged. An unknown value raises {invalid_config, nil_fields, Value}.

Changed

  • A value that matches no member of a union of only literals (an enum such as red | green | blue) now gives one type_mismatch error whose ctx has allowed_values (the JSON values a client may send, such as <<"red">> and null), instead of a no_match error with one sub-error per literal. JSON only.

Fixed

  • The per-branch errors of a no_match union error are now in the order the branches are declared, not reversed.
  • The JSON Schema of a typed map, such as #{binary() => integer()}, now describes its values: additionalProperties holds the value schema instead of true (an anyOf when the map has several typed fields, still true for term() values). When the map has no literal fields and its key type is constrained, such as a union of atoms, the key schema is emitted as propertyNames.
  • A not_matched_fields error, such as an empty map given to #{binary() := binary()} (Elixir %{String.t() => String.t()}), now has a message in its ctx explaining that the map needs a matching key and that an optional key (=> / optional(...)) allows an empty map.
  • JSON Schema generation no longer crashes with type_not_supported on a typed map whose key is a remote string type, such as Elixir %{optional(String.t()) => String.t()} or Erlang #{unicode:unicode_binary() => binary()}.

[0.15.0] - 2026-10-30

Added

  • Per-operation OpenAPI security. The Doc map given to spectra_openapi:endpoint/3 and spectra_openapi:webhook/3 now accepts a security key holding a list of Security Requirement Objects, emitted on that operation alone and overriding the global security default; security => [] opts the operation out of the global requirement entirely. This is what lets an API authenticate inbound calls one way and sign its outgoing webhooks another — previously a global requirement decorated every webhook operation with no way to override it.
  • OpenAPI 3.1 webhooks. New spectra_openapi:webhook/2,3 builds a webhook specification, and new spectra_openapi:to_openapi/4 generates a spec from both endpoints and webhooks, emitting the latter under the document's top-level webhooks key. A webhook is keyed by an event name rather than a URL path, because the consumer owns the URL; the direction is inverted relative to an endpoint, so the request body is the payload the API sends and the responses describe what the consumer returns. add_response/2, with_request_body/3,4 and with_parameter/3 accept webhooks as well as endpoints, and webhook schemas share components/schemas with endpoints. with_parameter/3 raises {parameter_location_not_supported_on_webhook, Name, In} for path and query parameters, which can never be satisfied on a webhook. endpoints_to_openapi/2,3 are unchanged and emit no webhooks key. The author has not reviewed the webhook support yet.

[0.14.2] - 2026-10-01

Fixed

  • A union of undefined (or nil) with two or more non-literal types, such as binary() | string() | undefined, put the missing value into its JSON Schema as anyOf: [..., {enum: [null]}]. The value is never encoded as null: in a record field or map value it is left out, and the field is already not required. The union now gets the same schema as without the undefined, as T | undefined already did, from @baronbosse (#196). Thank you!

  • Union members with identical schemas are no longer repeated in anyOf, and a union whose members all produce the same schema is that schema rather than a one-armed anyOf. binary() | string() is {"type": "string"} instead of anyOf: [{"type": "string"}, {"type": "string"}], from @baronbosse (#196). Thank you!

[0.14.1] - 2026-09-14

Fixed

  • Encoding a struct type (a map type with a __struct__ field, e.g. from Elixir) with non-map data (a string, integer, list, or atom) crashed with a raw badmap error instead of returning {error, [spectra:error()]}. Plain map types, list types, record types, and decoding were unaffected — only the struct branch of to_json/4 was missing its is_map/1 guard.

[0.14.0] - 2026-09-11

Fixed

  • -spectra() doc annotations (title, description, deprecated, examples, examples_function) are no longer dropped when a type is inlined into another schema. Only the type that schema generation was entered with kept its annotations. Every type resolved while inlining lost them silently, so deprecated => true on a type used as a map field produced nothing in the output. This covers map field values, record fields, union branches, list and non-empty list elements, optional map values, and remote types from other modules.
  • type_parameters were unaffected and keep working alongside a doc annotation on the same type.

Changed

  • Generated JSON Schema and OpenAPI output changes for annotated sub-schemas: nested properties now carry the titles, descriptions, deprecation flags and examples their types declare.
  • Where a type alias and the type it resolves to set the same key, the annotation nearest the use site wins. This matches how an alias of a documented record already behaved.
  • examples on an annotated type are now validated and converted at every position the type is inlined into, not only when it is the entry point. An example that does not encode as its own type raises {invalid_example, Type, Term, Errors}, which previously surfaced only if that type was used as an entry point.
  • examples_function is likewise invoked once per position the type appears in, rather than once per schema. Keep it cheap and free of side effects.

Three positions still cannot carry a nested annotation, all unchanged by this release: a union whose members all resolve to literals (it collapses to one enum schema), a type whose schema comes from a custom codec, and a parameterized type such as -type box(T) :: .... See the README for details.

[0.13.4] - 2026-06-14

Fixed

  • Encoding an iolist() or iodata() value that is not a valid iolist (e.g. a list containing a byte outside 0..255 such as [-1] or [256]) now returns a structured {error, [spectra:error()]} instead of crashing with a raw badarg from erlang:iolist_to_binary/1. This also fixes a union-disambiguation inconsistency where such a value produced a different exception type from JSON encoding than from schema generation.

[0.13.3] - 2026-06-14

Fixed

  • JSON schema no longer marks nilable exact map/struct fields as required. The schema generator treated every exact (:=) map field as required based on the field kind alone, so a field whose type can be missing (e.g. field | nil / field | undefined) — including Elixir struct fields, which all emit as exact — was wrongly listed in required even though the decoder tolerates its absence. An exact field is now required only when its type cannot be missing, matching the decoder and the record schema path.

[0.13.2] - 2026-06-12

Added

  • OpenAPI security schemes. spectra_openapi:endpoints_to_openapi/2,3 now accepts two optional metadata keys: security_schemes (a map of named Security Scheme Objects, emitted under components.securitySchemes) and security (a list of Security Requirement Objects, emitted at the top level). This is what makes Swagger UI render the Authorize button. Scheme values are passed through as JSON values (like schemas), so any scheme type works — apiKey, http/bearer, oauth2, openIdConnect.

[0.13.1] - 2026-05-07

Added

  • Fields excluded by the only filter on records now receive their Erlang record default value (e.g. age = 1) instead of always being set to undefined. Included fields remain mandatory and unchanged.

Fixed

  • field_aliases and only transforms declared with -spectra() are now correctly propagated when the type body is a type reference (-type t() :: other_type(), remote types, or record refs). Previously these transforms were silently dropped at the reference boundary.

[0.13.0] - 2026-05-07

Added

  • field_aliases support in the -spectra() attribute for records and map types. Use field_aliases => #{internal_name => <<"external_name">>} to rename fields in JSON, OpenAPI, and other encoded formats without changing the Erlang record or map key names.

[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