Skip to main content

nu_command/formats/from/
json.rs

1use std::io::{BufRead, Cursor};
2
3use nu_engine::command_prelude::*;
4use nu_protocol::{
5    ListStream, Signals,
6    shell_error::{generic::GenericError, io::IoError},
7};
8
9#[derive(Clone)]
10pub struct FromJson;
11
12impl Command for FromJson {
13    fn name(&self) -> &str {
14        "from json"
15    }
16
17    fn description(&self) -> &str {
18        "Convert JSON text into structured data."
19    }
20
21    fn signature(&self) -> nu_protocol::Signature {
22        Signature::build("from json")
23            .input_output_types(vec![(Type::String, Type::Any)])
24            .switch("objects", "Treat each line as a separate value.", Some('o'))
25            .switch(
26                "strict",
27                "Follow the json specification exactly.",
28                Some('s'),
29            )
30            .category(Category::Formats)
31    }
32
33    fn examples(&self) -> Vec<Example<'_>> {
34        vec![
35            Example {
36                example: r#"'{ "a": 1 }' | from json"#,
37                description: "Converts json formatted string to table.",
38                result: Some(Value::test_record(record! {
39                    "a" => Value::test_int(1),
40                })),
41            },
42            Example {
43                example: r#"'{ "a": 1, "b": [1, 2] }' | from json"#,
44                description: "Converts json formatted string to table.",
45                result: Some(Value::test_record(record! {
46                    "a" => Value::test_int(1),
47                    "b" => Value::test_list(vec![Value::test_int(1), Value::test_int(2)]),
48                })),
49            },
50            Example {
51                example: r#"'{ "a": 1, "b": 2 }' | from json -s"#,
52                description: "Parse json strictly which will error on comments and trailing commas.",
53                result: Some(Value::test_record(record! {
54                    "a" => Value::test_int(1),
55                    "b" => Value::test_int(2),
56                })),
57            },
58            Example {
59                example: r#"'{ "a": 1 }
60{ "b": 2 }' | from json --objects"#,
61                description: "Parse a stream of line-delimited JSON values.",
62                result: Some(Value::test_list(vec![
63                    Value::test_record(record! {"a" => Value::test_int(1)}),
64                    Value::test_record(record! {"b" => Value::test_int(2)}),
65                ])),
66            },
67        ]
68    }
69
70    fn run(
71        &self,
72        engine_state: &EngineState,
73        stack: &mut Stack,
74        call: &Call,
75        mut input: PipelineData,
76    ) -> Result<PipelineData, ShellError> {
77        let span = call.head;
78
79        let strict = call.has_flag(engine_state, stack, "strict")?;
80        let metadata = input.take_metadata().map(|md| md.with_content_type(None));
81
82        // TODO: turn this into a structured underline of the nu_json error
83        if call.has_flag(engine_state, stack, "objects")? {
84            // Return a stream of JSON values, one for each non-empty line
85            match input {
86                PipelineData::Value(Value::String { val, .. }, ..) => {
87                    Ok(PipelineData::list_stream(
88                        read_json_lines(
89                            Cursor::new(val),
90                            span,
91                            strict,
92                            engine_state.signals().clone(),
93                        ),
94                        metadata,
95                    ))
96                }
97                PipelineData::ByteStream(stream, ..)
98                    if stream.type_() != ByteStreamType::Binary =>
99                {
100                    if let Some(reader) = stream.reader() {
101                        Ok(PipelineData::list_stream(
102                            read_json_lines(reader, span, strict, Signals::empty()),
103                            metadata,
104                        ))
105                    } else {
106                        Ok(PipelineData::empty())
107                    }
108                }
109                _ => Err(ShellError::OnlySupportsThisInputType {
110                    exp_input_type: "string".into(),
111                    wrong_type: input.get_type().to_string(),
112                    dst_span: call.head,
113                    src_span: input.span().unwrap_or(call.head),
114                }),
115            }
116        } else {
117            // Return a single JSON value
118            let (string_input, span, ..) = input.collect_string_strict(span)?;
119
120            if string_input.is_empty() {
121                return Ok(Value::nothing(span).into_pipeline_data());
122            }
123
124            Ok(try_str_to_value(&string_input, span, strict)?
125                .into_pipeline_data_with_metadata(metadata))
126        }
127    }
128}
129
130/// Create a stream of values from a reader that produces line-delimited JSON
131fn read_json_lines(
132    input: impl BufRead + Send + 'static,
133    span: Span,
134    strict: bool,
135    signals: Signals,
136) -> ListStream {
137    let iter = input
138        .lines()
139        .filter(|line| line.as_ref().is_ok_and(|line| !line.trim().is_empty()) || line.is_err())
140        .map(move |line| {
141            let line = line.map_err(|err| IoError::new(err, span, None))?;
142            try_str_to_value(&line, span, strict)
143        })
144        .map(move |result| result.unwrap_or_else(|err| Value::error(err, span)));
145
146    ListStream::new(iter, span, signals)
147}
148
149pub fn try_str_to_value(input: &str, span: Span, strict: bool) -> Result<Value, ShellError> {
150    match strict {
151        true => try_str_to_value_impl(
152            input,
153            span,
154            |s| serde_json::from_str(s),
155            |err| err.is_syntax().then_some((err.line(), err.column())),
156        ),
157        false => try_str_to_value_impl(input, span, nu_json::from_str, |err| match err {
158            nu_json::Error::Syntax(_, row, col) => Some((*row, *col)),
159            _ => None,
160        }),
161    }
162}
163
164#[inline]
165fn try_str_to_value_impl<E: std::error::Error>(
166    input: &str,
167    span: Span,
168    parser: impl Fn(&str) -> Result<nu_json::Value, E>,
169    on_syntax_err: impl Fn(&E) -> Option<(usize, usize)>,
170) -> Result<Value, ShellError> {
171    match parser(input) {
172        Ok(value) => Ok(value.into_value(span)),
173        Err(err) => match on_syntax_err(&err) {
174            Some((row, col)) => {
175                let label = err.to_string();
176                let label_span = Span::from_row_column(row, col, input);
177                Err(ShellError::Generic(
178                    GenericError::new(
179                        "Error while parsing JSON text",
180                        "error parsing JSON text",
181                        span,
182                    )
183                    .with_inner([ShellError::OutsideSpannedLabeledError {
184                        src: input.into(),
185                        error: "Error while parsing JSON text".into(),
186                        msg: label,
187                        span: label_span,
188                    }]),
189                ))
190            }
191            None => Err(ShellError::CantConvert {
192                to_type: format!("structured json data ({err})"),
193                from_type: "string".into(),
194                span,
195                help: None,
196            }),
197        },
198    }
199}
200
201#[cfg(test)]
202mod test {
203    use super::*;
204
205    #[test]
206    fn test_examples() -> nu_test_support::Result {
207        nu_test_support::test().examples(FromJson)
208    }
209}