Packages
Complete Stripe SDK for Elixir with parity to the official Ruby SDK. V1+V2 coverage (193 services, 320 resource structs, 525 documented params). Per-event modules, Finch HTTP/2, RustyJSON, automatic retries, OAuth, webhooks, telemetry, per-client config, streaming pagination.
Current section
Files
Jump to
Current section
Files
lib/stripe/deserializer.ex
defmodule Stripe.Deserializer do
@moduledoc """
Deserializes JSON strings and raw maps into typed Stripe structs.
Used internally by the client to cast API responses. Also provides
`from_json/1` for manual deserialization of JSON payloads.
"""
alias Stripe.ObjectTypes
@doc """
Decode a JSON string and cast into typed structs.
"""
@spec from_json(String.t(), keyword()) :: {:ok, term()} | {:error, term()}
def from_json(json_string, opts \\ []) when is_binary(json_string) do
case JSON.decode(json_string) do
{:ok, data} -> {:ok, cast(data, opts)}
{:error, _} = error -> error
end
end
@spec cast(map() | list() | term(), keyword()) :: struct() | map() | list() | term()
def cast(data, opts \\ [])
# V1 list
def cast(%{"object" => "list"} = data, opts) do
items = data["data"] |> List.wrap() |> Enum.map(&cast(&1, opts))
%Stripe.ListObject{
object: data["object"],
data: items,
has_more: data["has_more"],
url: data["url"]
}
end
# V1 search result
def cast(%{"object" => "search_result"} = data, opts) do
items = data["data"] |> List.wrap() |> Enum.map(&cast(&1, opts))
%Stripe.SearchResult{
object: data["object"],
data: items,
has_more: data["has_more"],
url: data["url"],
next_page: data["next_page"],
total_count: data["total_count"]
}
end
# V2 list (detected by next_page_url key + data being a list)
def cast(%{"next_page_url" => _, "data" => items} = data, opts) when is_list(items) do
cast_items = Enum.map(items, &cast(&1, opts))
%Stripe.V2.ListObject{
data: cast_items,
next_page_url: data["next_page_url"]
}
end
# Object with known type
def cast(%{"object" => object_name} = data, opts) do
api_mode = Keyword.get(opts, :api_mode, :v1)
case resolve_module(object_name, api_mode) do
nil -> data
module -> populate_struct(module, data, opts)
end
end
# Map without "object" key — return raw
def cast(%{} = data, _opts), do: data
# Top-level list — cast each element
def cast(data, opts) when is_list(data), do: Enum.map(data, &cast(&1, opts))
# Scalars (string, integer, nil, etc.) — passthrough
def cast(data, _opts), do: data
# -- Private ----------------------------------------------------------------
defp resolve_module(name, :v2) do
Map.get(ObjectTypes.v2_object_name_to_module(), name) ||
Map.get(ObjectTypes.object_name_to_module(), name)
end
defp resolve_module(name, _v1) do
Map.get(ObjectTypes.object_name_to_module(), name)
end
defp populate_struct(module, data, opts) do
Code.ensure_loaded!(module)
nested_fields =
if function_exported?(module, :__nested_fields__, 0),
do: module.__nested_fields__(),
else: %{}
to_struct(module, data, nested_fields, opts)
end
defp to_struct(module, data, nested_fields, opts) do
module.__struct__()
|> Map.keys()
|> List.delete(:__struct__)
|> Enum.map(fn atom_key ->
string_key = Atom.to_string(atom_key)
raw = Map.get(data, string_key)
{atom_key, cast_field(string_key, raw, nested_fields, opts)}
end)
|> then(&struct(module, &1))
end
defp cast_field(_key, nil, _nested_fields, _opts), do: nil
# Field has generated nested-shape metadata and the value is present.
defp cast_field(key, raw, nested_fields, opts) when is_map_key(nested_fields, key) do
nested_fields
|> Map.fetch!(key)
|> cast_with_metadata(raw, opts)
end
# Expanded object (map with "object" key, no nested metadata match) → recursive cast
defp cast_field(_key, %{"object" => _} = raw, _nested_fields, opts) do
cast(raw, opts)
end
# List without nested metadata → cast each element that has an "object" key
defp cast_field(_key, raw, _nested_fields, opts) when is_list(raw) do
Enum.map(raw, fn
%{"object" => _} = item -> cast(item, opts)
other -> other
end)
end
# Scalar passthrough
defp cast_field(_key, raw, _nested_fields, _opts), do: raw
defp cast_with_metadata(_metadata, nil, _opts), do: nil
defp cast_with_metadata({:list, metadata}, raw, opts) when is_list(raw) do
Enum.map(raw, &cast_with_metadata(metadata, &1, opts))
end
defp cast_with_metadata({:map, metadata}, raw, opts) when is_map(raw) do
Map.new(raw, fn {key, value} -> {key, cast_with_metadata(metadata, value, opts)} end)
end
defp cast_with_metadata({:resource, module}, %{} = raw, opts) do
populate_struct(module, raw, opts)
end
defp cast_with_metadata(%{fields: fields}, %{} = raw, opts) do
known =
fields
|> Enum.map(fn {string_key, metadata} ->
atom_key = String.to_atom(string_key)
raw_value = Map.get(raw, string_key)
{atom_key, cast_with_metadata(metadata, raw_value, opts)}
end)
|> Map.new()
unknown =
raw
|> Enum.reject(fn {key, _value} -> Map.has_key?(fields, key) end)
|> Map.new()
Map.merge(unknown, known)
end
defp cast_with_metadata(:scalar, %{"object" => _} = raw, opts), do: cast(raw, opts)
defp cast_with_metadata(_metadata, raw, _opts), do: raw
end