defmodule RustyJson do @moduledoc """ A high-performance JSON library for Elixir powered by Rust NIFs. RustyJson is designed as a **drop-in replacement for Jason** with significantly better performance characteristics: - **3-6x faster encoding** for medium/large payloads - **10-20x lower memory usage** during encoding - **Full JSON spec compliance** (RFC 8259) - **Native type support** for DateTime, Decimal, URI, and more ## Quick Start # Encoding iex> RustyJson.encode!(%{name: "Alice", age: 30}) ~s({"age":30,"name":"Alice"}) # Decoding iex> RustyJson.decode!(~s({"name":"Alice","age":30})) %{"age" => 30, "name" => "Alice"} # Pretty printing iex> RustyJson.encode!(%{items: [1, 2, 3]}, pretty: true) \""" { "items": [ 1, 2, 3 ] } \""" ## Phoenix Integration Configure Phoenix to use RustyJson as the JSON library: # config/config.exs config :phoenix, :json_library, RustyJson ## Why RustyJson? Traditional JSON libraries in Elixir create many intermediate binary allocations during encoding, which pressures the garbage collector. RustyJson eliminates this by walking the Erlang term tree directly in Rust and writing to a single buffer. For a detailed comparison, see the [README](readme.html). ## Module Overview | Module | Description | |--------|-------------| | `RustyJson` | Main encoding/decoding API | | `RustyJson.Encoder` | Protocol for custom type encoding | | `RustyJson.Fragment` | Pre-encoded JSON injection | | `RustyJson.Formatter` | JSON pretty-printing utilities | | `RustyJson.DecodeError` | Decoding error exception | | `RustyJson.EncodeError` | Encoding error exception | ## Built-in Type Support RustyJson natively handles these Elixir types without protocol overhead: | Elixir Type | JSON Output | Example | |-------------|-------------|---------| | `map` | object | `%{a: 1}` → `{"a":1}` | | `list` | array | `[1, 2]` → `[1,2]` | | `tuple` | array | `{1, 2}` → `[1,2]` | | `binary` | string | `"hello"` → `"hello"` | | `integer` | number | `42` → `42` | | `float` | number | `3.14` → `3.14` | | `true/false` | boolean | `true` → `true` | | `nil` | null | `nil` → `null` | | `atom` | string | `:hello` → `"hello"` | | `DateTime` | ISO8601 string | `~U[2024-01-15 14:30:00Z]` → `"2024-01-15T14:30:00Z"` | | `NaiveDateTime` | ISO8601 string | `~N[2024-01-15 14:30:00]` → `"2024-01-15T14:30:00"` | | `Date` | ISO8601 string | `~D[2024-01-15]` → `"2024-01-15"` | | `Time` | ISO8601 string | `~T[14:30:00]` → `"14:30:00"` | | `Decimal` | string | `Decimal.new("123.45")` → `"123.45"` | | `URI` | string | `URI.parse("https://example.com")` → `"https://example.com"` | | `MapSet` | array | `MapSet.new([1, 2])` → `[1,2]` | | `Range` | object | `1..10` → `{"first":1,"last":10,"step":1}` | | structs | object | `%User{name: "Alice"}` → `{"name":"Alice"}` | ## Escape Modes RustyJson supports multiple escape modes for different security contexts: | Mode | Description | Use Case | |------|-------------|----------| | `:json` | Standard JSON escaping (default) | General use | | `:html_safe` | Escapes `<`, `>`, `&` as `\\uXXXX` | HTML embedding | | `:javascript_safe` | Escapes line/paragraph separators | JavaScript strings | | `:unicode_safe` | Escapes all non-ASCII as `\\uXXXX` | ASCII-only output | ## Performance Tips 1. **Skip the protocol** (default): For maximum speed, don't use `protocol: true` unless you have custom `RustyJson.Encoder` implementations. 2. **Use lean mode**: If you don't have DateTime/Decimal types, use `lean: true` to skip struct type detection in Rust. 3. **Use compression**: For large payloads over the network, `compress: :gzip` reduces output size 5-10x. 4. **Avoid atom keys in decode**: Using `keys: :atoms!` creates atoms from untrusted input, which can exhaust the atom table. """ @typedoc """ Supported compression algorithms for encoding. - `:gzip` - Standard gzip compression - `:none` - No compression (default) """ @type compression_algorithm :: :gzip | :none @typedoc """ Compression level from 0 (fastest, least compression) to 9 (slowest, best compression). """ @type compression_level :: 0..9 @typedoc """ Compression options tuple. Can be specified as: - `:gzip` - Use default compression level - `{:gzip, level}` - Use specific compression level (0-9) - `:none` - No compression """ @type compression_option :: :gzip | {:gzip, compression_level()} | :none @typedoc """ Internal compression options format passed to NIF. """ @type compression_options :: {compression_algorithm(), compression_level() | nil} @typedoc """ Options for decoding JSON object keys. - `:strings` - Keep keys as strings (default, safe) - `:atoms` - Convert to atoms only if they already exist (safe) - `:atoms!` - Always convert to atoms, creating new ones (unsafe with untrusted input) """ @type keys :: :strings | :atoms | :atoms! @typedoc """ Escape mode for JSON string encoding. - `:json` - Standard JSON escaping (default) - `:html_safe` - Also escape `<`, `>`, `&` for safe HTML embedding - `:javascript_safe` - Also escape line/paragraph separators (U+2028, U+2029) - `:unicode_safe` - Escape all non-ASCII characters as `\\uXXXX` """ @type escape_mode :: :json | :html_safe | :javascript_safe | :unicode_safe @typedoc """ Options for `encode/2` and `encode!/2`. - `:pretty` - Pretty print with indentation. `true` for 2 spaces, or an integer for custom. - `:escape` - Escape mode (see `t:escape_mode/0`). Default: `:json` - `:compress` - Compression (see `t:compression_option/0`). Default: `:none` - `:protocol` - Use `RustyJson.Encoder` protocol. Default: `false` - `:lean` - Skip special struct handling. Default: `false` """ @type encode_opt :: {:pretty, boolean() | pos_integer()} | {:escape, escape_mode()} | {:compress, compression_option()} | {:protocol, boolean()} | {:lean, boolean()} @typedoc """ Options for `decode/2` and `decode!/2`. - `:keys` - How to handle object keys (see `t:keys/0`). Default: `:strings` """ @type decode_opt :: {:keys, keys()} source_url = Mix.Project.config()[:source_url] version = Mix.Project.config()[:version] # Check env var or application config for force_build force_build? = System.get_env("FORCE_RUSTYJSON_BUILD") in ["1", "true"] or Application.compile_env(:rustler_precompiled, :force_build, [])[:rustyjson] == true use RustlerPrecompiled, otp_app: :rustyjson, base_url: "#{source_url}/releases/download/v#{version}", force_build: force_build?, nif_versions: ["2.15", "2.16", "2.17"], targets: RustlerPrecompiled.Config.default_targets(), version: version # NIF stubs - these are replaced by Rustler at runtime @doc false @spec nif_encode_direct( term(), non_neg_integer() | nil, compression_options(), boolean(), atom() | nil ) :: String.t() defp nif_encode_direct(_input, _indent, _compression, _lean, _escape), do: :erlang.nif_error(:nif_not_loaded) @doc false @spec nif_decode(String.t()) :: term() defp nif_decode(_input), do: :erlang.nif_error(:nif_not_loaded) # ============================================================================ # Encoding API # ============================================================================ @doc """ Encodes an Elixir term to a JSON string. Returns `{:ok, json}` on success or `{:error, reason}` on failure. ## Options * `:pretty` - Pretty print with indentation. `true` uses 2 spaces, or pass an integer for custom spacing. Default: `false` * `:escape` - Escape mode for special characters. One of `:json` (default), `:html_safe`, `:javascript_safe`, or `:unicode_safe`. See `t:escape_mode/0`. * `:compress` - Compression algorithm. `:gzip` or `{:gzip, 0..9}` for specific level. Default: `:none` * `:protocol` - Enable `RustyJson.Encoder` protocol for custom types. Default: `false` * `:lean` - Skip struct type detection (DateTime, Decimal, etc. encoded as raw maps). Default: `false` ## Examples iex> RustyJson.encode(%{name: "Alice", scores: [95, 87, 92]}) {:ok, ~s({"name":"Alice","scores":[95,87,92]})} iex> RustyJson.encode(%{valid: true}, pretty: true) {:ok, \"{\\n \\\"valid\\\": true\\n}\"} iex> RustyJson.encode("invalid UTF-8: " <> <<0xFF>>) {:error, "Failed to decode binary as UTF-8"} ## Error Handling Common error cases: - Invalid UTF-8 binary - Non-finite float (NaN, Infinity) - Circular references (will cause stack overflow) See `encode!/2` for a version that raises on error. """ @spec encode(term(), [encode_opt()]) :: {:ok, String.t()} | {:error, String.t()} def encode(input, opts \\ []) do {:ok, encode!(input, opts)} rescue e in [RustyJson.EncodeError, ErlangError, ArgumentError] -> {:error, error_message(e)} end @doc """ Encodes an Elixir term to a JSON string, raising on error. Same as `encode/2` but raises `RustyJson.EncodeError` on failure. ## Options See `encode/2` for available options. ## Examples iex> RustyJson.encode!(%{hello: "world"}) ~s({"hello":"world"}) iex> RustyJson.encode!([1, 2, 3], pretty: 4) \""" [ 1, 2, 3 ] \""" iex> RustyJson.encode!(~D[2024-01-15]) ~s("2024-01-15") iex> RustyJson.encode!(%{html: "