AshJsonApi converts Ash errors into JSON:API error objects. This topic covers how that conversion works, how to customize it, and the available configuration options.
Error Format
Every error response follows the JSON:API error object format:
{
"errors": [
{
"id": "a1b2c3d4-...",
"status": "422",
"code": "invalid_attribute",
"title": "InvalidAttribute",
"detail": "must be present",
"source": {
"pointer": "/data/attributes/name"
}
}
]
}The AshJsonApi.ToJsonApiError Protocol
AshJsonApi uses the AshJsonApi.ToJsonApiError protocol to convert Ash exceptions into AshJsonApi.Error structs. Built-in implementations are provided for common Ash errors such as Ash.Error.Changes.InvalidChanges, Ash.Error.Query.NotFound, Ash.Error.Forbidden.Policy, and others.
If your application raises a custom Ash exception and you want it to produce a specific JSON:API error, implement the protocol:
defimpl AshJsonApi.ToJsonApiError, for: MyApp.Errors.PaymentRequired do
def to_json_api_error(error) do
%AshJsonApi.Error{
id: Ash.UUID.generate(),
status_code: 402,
code: "payment_required",
title: "PaymentRequired",
detail: error.message,
meta: %{}
}
end
endThe AshJsonApi.Error struct has the following fields:
| Field | Description |
|---|---|
id | Unique identifier for this error occurrence |
status_code | HTTP status code (integer) |
code | Machine-readable error code string |
title | Human-readable error title |
detail | Human-readable explanation specific to this occurrence |
source_pointer | JSON Pointer to the source of the error (e.g. /data/attributes/name) |
source_parameter | Query parameter that caused the error |
meta | Arbitrary metadata map |
about | Link to further information about this error |
log_level | Log level for this error (default: :debug) |
internal_description | Internal description used for logging, not sent to clients |
Transforming Errors with error_handler
The error_handler domain option lets you intercept and transform any AshJsonApi.Error struct before it is sent to the client. This is useful for sanitizing error messages, adding metadata, translating error text, or applying any other cross-cutting transformation.
Configure it in your domain as an MFA:
defmodule MyApp.Domain do
use Ash.Domain, extensions: [AshJsonApi.Domain]
json_api do
error_handler {MyApp.JsonApiErrorHandler, :handle_error, []}
end
endThe handler receives the AshJsonApi.Error struct and a context map, and must return a modified AshJsonApi.Error struct:
defmodule MyApp.JsonApiErrorHandler do
def handle_error(error, _context) do
# Sanitize internal details from 500 errors
if error.status_code >= 500 do
%{error | detail: "An internal error occurred. Please try again later."}
else
error
end
end
endThe context map contains:
| Key | Description |
|---|---|
:domain | The domain module handling the request |
:resource | The resource module associated with the request (may be nil) |
Example: Translating Error Messages
defmodule MyApp.JsonApiErrorHandler do
def handle_error(error, _context) do
%{error | detail: MyApp.Gettext.translate_error(error.code, error.detail)}
end
endExample: Adding Custom Metadata
defmodule MyApp.JsonApiErrorHandler do
def handle_error(error, %{domain: domain}) do
%{error | meta: Map.put(error.meta || %{}, :api_version, "v2")}
end
endExample: Context-Specific Handling
defmodule MyApp.JsonApiErrorHandler do
def handle_error(error, %{resource: resource}) do
case resource do
MyApp.PaymentResource ->
%{error | detail: MyApp.Payments.format_error(error)}
_ ->
error
end
end
endConfiguration Options
show_raised_errors?
By default, if an error is raised (i.e. an unexpected exception, not a structured Ash error), AshJsonApi returns a generic error message with only a UUID for reference. This prevents leaking internal implementation details.
Set show_raised_errors? true to include the full exception in the response — useful during development:
json_api do
show_raised_errors? true
endlog_errors?
Controls whether errors are logged. Defaults to true.
json_api do
log_errors? false
endPolicy Breakdown Details
By default, authorization failures return a generic "forbidden" message. To include a breakdown of which policies failed (useful for debugging), set this in your application config:
# config/dev.exs
config :ash_json_api, :policies, show_policy_breakdowns?: trueWarning: Do not enable this in production, as it may expose details about your authorization logic.