axum_extra/response/
erased_json.rs

1use std::sync::Arc;
2
3use axum::{
4    http::{header, HeaderValue, StatusCode},
5    response::{IntoResponse, Response},
6};
7use bytes::{BufMut, Bytes, BytesMut};
8use serde::Serialize;
9
10/// A response type that holds a JSON in serialized form.
11///
12/// This allows returning a borrowing type from a handler, or returning different response
13/// types as JSON from different branches inside a handler.
14///
15/// Like [`axum::Json`],
16/// if the [`Serialize`] implementation fails
17/// or if a map with non-string keys is used,
18/// a 500 response will be issued
19/// whose body is the error message in UTF-8.
20///
21/// This can be constructed using [`new`](ErasedJson::new)
22/// or the [`json!`](crate::json) macro.
23///
24/// # Example
25///
26/// ```rust
27/// # use axum::{response::IntoResponse};
28/// # use axum_extra::response::ErasedJson;
29/// async fn handler() -> ErasedJson {
30///     # let condition = true;
31///     # let foo = ();
32///     # let bar = vec![()];
33///     // ...
34///
35///     if condition {
36///         ErasedJson::new(&foo)
37///     } else {
38///         ErasedJson::new(&bar)
39///     }
40/// }
41/// ```
42#[cfg_attr(docsrs, doc(cfg(feature = "erased-json")))]
43#[derive(Clone, Debug)]
44#[must_use]
45pub struct ErasedJson(Result<Bytes, Arc<serde_json::Error>>);
46
47impl ErasedJson {
48    /// Create an `ErasedJson` by serializing a value with the compact formatter.
49    pub fn new<T: Serialize>(val: T) -> Self {
50        let mut bytes = BytesMut::with_capacity(128);
51        let result = match serde_json::to_writer((&mut bytes).writer(), &val) {
52            Ok(()) => Ok(bytes.freeze()),
53            Err(e) => Err(Arc::new(e)),
54        };
55        Self(result)
56    }
57
58    /// Create an `ErasedJson` by serializing a value with the pretty formatter.
59    pub fn pretty<T: Serialize>(val: T) -> Self {
60        let mut bytes = BytesMut::with_capacity(128);
61        let result = match serde_json::to_writer_pretty((&mut bytes).writer(), &val) {
62            Ok(()) => Ok(bytes.freeze()),
63            Err(e) => Err(Arc::new(e)),
64        };
65        Self(result)
66    }
67}
68
69impl IntoResponse for ErasedJson {
70    fn into_response(self) -> Response {
71        match self.0 {
72            Ok(bytes) => (
73                [(
74                    header::CONTENT_TYPE,
75                    HeaderValue::from_static(mime::APPLICATION_JSON.as_ref()),
76                )],
77                bytes,
78            )
79                .into_response(),
80            Err(err) => (StatusCode::INTERNAL_SERVER_ERROR, err.to_string()).into_response(),
81        }
82    }
83}
84
85/// Construct an [`ErasedJson`] response from a JSON literal.
86///
87/// A `Content-Type: application/json` header is automatically added.
88/// Any variable or expression implementing [`Serialize`]
89/// can be interpolated as a value in the literal.
90/// If the [`Serialize`] implementation fails,
91/// or if a map with non-string keys is used,
92/// a 500 response will be issued
93/// whose body is the error message in UTF-8.
94///
95/// Internally,
96/// this function uses the [`typed_json::json!`] macro,
97/// allowing it to perform far fewer allocations
98/// than a dynamic macro like [`serde_json::json!`] would –
99/// it's equivalent to if you had just written
100/// `derive(Serialize)` on a struct.
101///
102/// # Examples
103///
104/// ```
105/// use axum::{
106///     Router,
107///     extract::Path,
108///     response::Response,
109///     routing::get,
110/// };
111/// use axum_extra::response::ErasedJson;
112///
113/// async fn get_user(Path(user_id) : Path<u64>) -> ErasedJson {
114///     let user_name = find_user_name(user_id).await;
115///     axum_extra::json!({ "name": user_name })
116/// }
117///
118/// async fn find_user_name(user_id: u64) -> String {
119///     // ...
120///     # unimplemented!()
121/// }
122///
123/// let app = Router::new().route("/users/{id}", get(get_user));
124/// # let _: Router = app;
125/// ```
126///
127/// Trailing commas are allowed in both arrays and objects.
128///
129/// ```
130/// let response = axum_extra::json!(["trailing",]);
131/// ```
132#[macro_export]
133macro_rules! json {
134    ($($t:tt)*) => {
135        $crate::response::ErasedJson::new(
136            $crate::response::__private_erased_json::typed_json::json!($($t)*)
137        )
138    }
139}
140
141/// Not public API. Re-exported as `crate::response::__private_erased_json`.
142#[doc(hidden)]
143pub mod private {
144    pub use typed_json;
145}