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}