Skip to content

Commit

Permalink
Add axum_extra::json! (#2962)
Browse files Browse the repository at this point in the history
  • Loading branch information
SabrinaJewson authored and jplatte committed Nov 14, 2024
1 parent 5b6d1ca commit b71d4fa
Show file tree
Hide file tree
Showing 5 changed files with 91 additions and 2 deletions.
6 changes: 6 additions & 0 deletions axum-extra/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,12 @@ 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

- **added:** Add `json!` for easy construction of JSON responses ([#2962])

[#2962]: https://github.com/tokio-rs/axum/pull/2962

# 0.9.4

- **added:** The `response::Attachment` type ([#2789])
Expand Down
6 changes: 4 additions & 2 deletions axum-extra/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ cookie = ["dep:cookie"]
cookie-private = ["cookie", "cookie?/private"]
cookie-signed = ["cookie", "cookie?/signed"]
cookie-key-expansion = ["cookie", "cookie?/key-expansion"]
erased-json = ["dep:serde_json"]
erased-json = ["dep:serde_json", "dep:typed-json"]
form = ["dep:serde_html_form"]
json-deserializer = ["dep:serde_json", "dep:serde_path_to_error"]
json-lines = [
Expand Down Expand Up @@ -69,9 +69,11 @@ tokio = { version = "1.19", optional = true }
tokio-stream = { version = "0.1.9", optional = true }
tokio-util = { version = "0.7", optional = true }
tracing = { version = "0.1.37", default-features = false, optional = true }
typed-json = { version = "0.1.1", optional = true }

[dev-dependencies]
axum = { path = "../axum", version = "0.7.2" }
axum = { path = "../axum", features = ["macros"] }
axum-macros = { path = "../axum-macros", features = ["__private"] }
hyper = "1.0.0"
reqwest = { version = "0.12", default-features = false, features = ["json", "stream", "multipart"] }
serde = { version = "1.0", features = ["derive"] }
Expand Down
71 changes: 71 additions & 0 deletions axum-extra/src/response/erased_json.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,15 @@ use serde::Serialize;
/// This allows returning a borrowing type from a handler, or returning different response
/// types as JSON from different branches inside a handler.
///
/// Like [`axum::Json`],
/// if the [`Serialize`] implementation fails
/// or if a map with non-string keys is used,
/// a 500 response will be issued
/// whose body is the error message in UTF-8.
///
/// This can be constructed using [`new`](ErasedJson::new)
/// or the [`json!`](crate::json) macro.
///
/// # Example
///
/// ```rust
Expand Down Expand Up @@ -72,3 +81,65 @@ impl IntoResponse for ErasedJson {
}
}
}

/// Construct an [`ErasedJson`] response from a JSON literal.
///
/// A `Content-Type: application/json` header is automatically added.
/// Any variable or expression implementing [`Serialize`]
/// can be interpolated as a value in the literal.
/// If the [`Serialize`] implementation fails,
/// or if a map with non-string keys is used,
/// a 500 response will be issued
/// whose body is the error message in UTF-8.
///
/// Internally,
/// this function uses the [`typed_json::json!`] macro,
/// allowing it to perform far fewer allocations
/// than a dynamic macro like [`serde_json::json!`] would –
/// it's equivalent to if you had just written
/// `derive(Serialize)` on a struct.
///
/// # Examples
///
/// ```
/// use axum::{
/// Router,
/// extract::Path,
/// response::Response,
/// routing::get,
/// };
/// use axum_extra::response::ErasedJson;
///
/// async fn get_user(Path(user_id) : Path<u64>) -> ErasedJson {
/// let user_name = find_user_name(user_id).await;
/// axum_extra::json!({ "name": user_name })
/// }
///
/// async fn find_user_name(user_id: u64) -> String {
/// // ...
/// # unimplemented!()
/// }
///
/// let app = Router::new().route("/users/{id}", get(get_user));
/// # let _: Router = app;
/// ```
///
/// Trailing commas are allowed in both arrays and objects.
///
/// ```
/// let response = axum_extra::json!(["trailing",]);
/// ```
#[macro_export]
macro_rules! json {
($($t:tt)*) => {
$crate::response::ErasedJson::new(
$crate::response::__private_erased_json::typed_json::json!($($t)*)
)
}
}

/// Not public API. Re-exported as `crate::response::__private_erased_json`.
#[doc(hidden)]
pub mod private {
pub use typed_json;
}
5 changes: 5 additions & 0 deletions axum-extra/src/response/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,11 @@ pub mod multiple;
#[cfg(feature = "erased-json")]
pub use erased_json::ErasedJson;

/// _not_ public API
#[cfg(feature = "erased-json")]
#[doc(hidden)]
pub use erased_json::private as __private_erased_json;

#[cfg(feature = "json-lines")]
#[doc(no_inline)]
pub use crate::json_lines::JsonLines;
Expand Down
5 changes: 5 additions & 0 deletions axum/src/json.rs
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,11 @@ use serde::{de::DeserializeOwned, Serialize};
/// When used as a response, it can serialize any type that implements [`serde::Serialize`] to
/// `JSON`, and will automatically set `Content-Type: application/json` header.
///
/// If the [`Serialize`] implementation decides to fail
/// or if a map with non-string keys is used,
/// a 500 response will be issued
/// whose body is the error message in UTF-8.
///
/// # Response example
///
/// ```
Expand Down

0 comments on commit b71d4fa

Please sign in to comment.