1use std::io::{BufRead, Cursor};
2
3use nu_engine::command_prelude::*;
4use nu_protocol::{
5 DEFAULT_ERROR_CONTEXT, ListStream, Signals,
6 shell_error::{generic::GenericError, io::IoError},
7 truncated_source_window,
8};
9
10#[derive(Clone)]
11pub struct FromJson;
12
13impl Command for FromJson {
14 fn name(&self) -> &str {
15 "from json"
16 }
17
18 fn description(&self) -> &str {
19 "Convert JSON text into structured data."
20 }
21
22 fn signature(&self) -> nu_protocol::Signature {
23 Signature::build("from json")
24 .input_output_types(vec![(Type::String, Type::Any)])
25 .switch("objects", "Treat each line as a separate value.", Some('o'))
26 .switch(
27 "strict",
28 "Follow the json specification exactly.",
29 Some('s'),
30 )
31 .category(Category::Formats)
32 }
33
34 fn examples(&self) -> Vec<Example<'_>> {
35 vec![
36 Example {
37 example: r#"'{ "a": 1 }' | from json"#,
38 description: "Converts json formatted string to table.",
39 result: Some(Value::test_record(record! {
40 "a" => Value::test_int(1),
41 })),
42 },
43 Example {
44 example: r#"'{ "a": 1, "b": [1, 2] }' | from json"#,
45 description: "Converts json formatted string to table.",
46 result: Some(Value::test_record(record! {
47 "a" => Value::test_int(1),
48 "b" => Value::test_list(vec![Value::test_int(1), Value::test_int(2)]),
49 })),
50 },
51 Example {
52 example: r#"'{ "a": 1, "b": 2 }' | from json -s"#,
53 description: "Parse json strictly which will error on comments and trailing commas.",
54 result: Some(Value::test_record(record! {
55 "a" => Value::test_int(1),
56 "b" => Value::test_int(2),
57 })),
58 },
59 Example {
60 example: r#"'{ "a": 1 }
61{ "b": 2 }' | from json --objects"#,
62 description: "Parse a stream of line-delimited JSON values.",
63 result: Some(Value::test_list(vec![
64 Value::test_record(record! {"a" => Value::test_int(1)}),
65 Value::test_record(record! {"b" => Value::test_int(2)}),
66 ])),
67 },
68 ]
69 }
70
71 fn run(
72 &self,
73 engine_state: &EngineState,
74 stack: &mut Stack,
75 call: &Call,
76 mut input: PipelineData,
77 ) -> Result<PipelineData, ShellError> {
78 let span = call.head;
79
80 let strict = call.has_flag(engine_state, stack, "strict")?;
81 let metadata = input.take_metadata().map(|md| md.with_content_type(None));
82
83 if call.has_flag(engine_state, stack, "objects")? {
85 match input {
87 PipelineData::Value(Value::String { val, .. }, ..) => {
88 Ok(PipelineData::list_stream(
89 read_json_lines(
90 Cursor::new(val),
91 span,
92 strict,
93 engine_state.signals().clone(),
94 ),
95 metadata,
96 ))
97 }
98 PipelineData::ByteStream(stream, ..)
99 if stream.type_() != ByteStreamType::Binary =>
100 {
101 if let Some(reader) = stream.reader() {
102 Ok(PipelineData::list_stream(
103 read_json_lines(reader, span, strict, engine_state.signals().clone()),
104 metadata,
105 ))
106 } else {
107 Ok(PipelineData::empty())
108 }
109 }
110 _ => Err(ShellError::OnlySupportsThisInputType {
111 exp_input_type: "string".into(),
112 wrong_type: input.get_type().to_string(),
113 dst_span: call.head,
114 src_span: input.span().unwrap_or(call.head),
115 }),
116 }
117 } else {
118 let (string_input, span, ..) = input.collect_string_strict(span)?;
120
121 if string_input.is_empty() {
122 return Ok(Value::nothing(span).into_pipeline_data());
123 }
124
125 Ok(
126 try_str_to_value(&string_input, span, strict, engine_state.signals())?
127 .into_pipeline_data_with_metadata(metadata),
128 )
129 }
130 }
131}
132
133fn read_json_lines(
135 input: impl BufRead + Send + 'static,
136 span: Span,
137 strict: bool,
138 signals: Signals,
139) -> ListStream {
140 let iter_signals = signals.clone();
141 let iter = input
142 .lines()
143 .filter(|line| line.as_ref().is_ok_and(|line| !line.trim().is_empty()) || line.is_err())
144 .map(move |line| {
145 let line = line.map_err(|err| IoError::new(err, span, None))?;
146 try_str_to_value(&line, span, strict, &iter_signals)
147 })
148 .map(move |result| result.unwrap_or_else(|err| Value::error(err, span)));
149
150 ListStream::new(iter, span, signals)
151}
152
153pub fn try_str_to_value(
154 input: &str,
155 span: Span,
156 strict: bool,
157 signals: &Signals,
158) -> Result<Value, ShellError> {
159 match strict {
160 true => try_str_to_value_impl(
161 input,
162 span,
163 signals,
164 |s| serde_json::from_str(s),
165 |err| err.is_syntax().then_some((err.line(), err.column())),
166 ),
167 false => try_str_to_value_impl(input, span, signals, nu_json::from_str, |err| match err {
168 nu_json::Error::Syntax(_, row, col) => Some((*row, *col)),
169 _ => None,
170 }),
171 }
172}
173
174#[inline]
175fn try_str_to_value_impl<E: std::error::Error>(
176 input: &str,
177 span: Span,
178 signals: &Signals,
179 parser: impl Fn(&str) -> Result<nu_json::Value, E>,
180 on_syntax_err: impl Fn(&E) -> Option<(usize, usize)>,
181) -> Result<Value, ShellError> {
182 match parser(input) {
183 Ok(value) => Ok(value.into_value(span)),
184 Err(err) => match on_syntax_err(&err) {
185 Some((row, col)) => {
186 let label = err.to_string();
187 let byte_span = Span::try_from_row_column(row, col, input, &span, signals)?;
188 let (src, label_span) =
189 truncated_source_window(input, byte_span, DEFAULT_ERROR_CONTEXT);
190 Err(ShellError::Generic(
191 GenericError::new(
192 "Error while parsing JSON text",
193 "error parsing JSON text",
194 span,
195 )
196 .with_inner([ShellError::OutsideSpannedLabeledError {
197 src,
198 error: "Error while parsing JSON text".into(),
199 msg: label,
200 span: label_span,
201 }]),
202 ))
203 }
204 None => Err(ShellError::CantConvert {
205 to_type: format!("structured json data ({err})"),
206 from_type: "string".into(),
207 span,
208 help: None,
209 }),
210 },
211 }
212}
213
214#[cfg(test)]
215mod test {
216 use super::*;
217
218 #[test]
219 fn test_examples() -> nu_test_support::Result {
220 nu_test_support::test().examples(FromJson)
221 }
222
223 #[test]
224 fn json_error_source_is_bounded() {
225 let mut valid_part = String::new();
227 valid_part.push('[');
228 for i in 0..2000 {
229 use std::fmt::Write;
230 write!(&mut valid_part, r#""line {i}","#).unwrap();
231 }
232 valid_part.push_str("broken]"); let signals = Signals::empty();
236 let result = try_str_to_value(&valid_part, Span::test_data(), true, &signals);
237 assert!(result.is_err(), "should fail to parse");
238
239 let err = result.unwrap_err();
240 match &err {
241 ShellError::Generic(GenericError { inner, .. }) => {
242 let inner_err = inner.first().expect("should have inner error");
243 match inner_err {
244 ShellError::OutsideSpannedLabeledError { src, .. } => {
245 assert!(
247 src.len() < 20_000,
248 "error source should be bounded, got {} bytes",
249 src.len()
250 );
251 }
252 other => panic!("expected OutsideSpannedLabeledError, got {other:?}"),
253 }
254 }
255 other => panic!("expected Generic error, got {other:?}"),
256 }
257 }
258
259 #[test]
260 fn json_error_source_not_entire_file() {
261 let mut input = String::with_capacity(50_000);
263 input.push('[');
264 input.push_str(&"0,".repeat(10_000));
265 input.push_str(":]"); let signals = Signals::empty();
268 let result = try_str_to_value(&input, Span::test_data(), true, &signals);
269 assert!(result.is_err());
270
271 let err = result.unwrap_err();
272 match &err {
273 ShellError::Generic(GenericError { inner, .. }) => {
274 let inner_err = inner.first().expect("should have inner error");
275 match inner_err {
276 ShellError::OutsideSpannedLabeledError { src, .. } => {
277 assert!(
279 src.len() < 20_000,
280 "error source should be bounded, got {} bytes",
281 src.len()
282 );
283 }
284 other => panic!("expected OutsideSpannedLabeledError, got {other:?}"),
285 }
286 }
287 other => panic!("expected Generic error, got {other:?}"),
288 }
289 }
290
291 #[test]
292 fn json_parse_success_not_affected() {
293 let input = r#"{"a": 1, "b": [2, 3]}"#;
294 let signals = Signals::empty();
295 let result = try_str_to_value(input, Span::test_data(), true, &signals);
296 assert!(result.is_ok(), "valid JSON should still parse");
297 }
298}