diff --git a/docs/reference/schemas/config/functions/json.md b/docs/reference/schemas/config/functions/json.md new file mode 100644 index 000000000..98bbfdcd4 --- /dev/null +++ b/docs/reference/schemas/config/functions/json.md @@ -0,0 +1,232 @@ +--- +description: Reference for the 'json' DSC configuration document function +ms.date: 10/11/2025 +ms.topic: reference +title: json +--- + +## Synopsis + +Converts a valid JSON string into a JSON data type. + +## Syntax + +```Syntax +json(arg1) +``` + +## Description + +The `json()` function parses a JSON string and returns the corresponding JSON data type. + +- The string must be a properly formatted JSON string. +- Returns the parsed JSON value (object, array, string, number, boolean, or null). + +This function is useful for converting JSON strings received from external sources or +stored as configuration into usable data structures. + +## Examples + +### Example 1 - Parse JSON object + +This example parses a JSON string containing an object into a usable object data type. + +```yaml +# json.example.1.dsc.config.yaml +$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json +resources: +- name: Echo + type: Microsoft.DSC.Debug/Echo + properties: + output: "[json('{\"name\":\"John\",\"age\":30}')]" +``` + +```bash +dsc config get --file json.example.1.dsc.config.yaml +``` + +```yaml +results: +- name: Echo + type: Microsoft.DSC.Debug/Echo + result: + actualState: + output: + name: John + age: 30 +messages: [] +hadErrors: false +``` + +### Example 2 - Parse JSON array + +This example parses a JSON string containing an array using [`json()`][00] and then +uses [`length()`][01] to count the elements. + +```yaml +# json.example.2.dsc.config.yaml +$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json +resources: +- name: Echo + type: Microsoft.DSC.Debug/Echo + properties: + output: "[length(json('[1,2,3,4,5]'))]" +``` + +```bash +dsc config get --file json.example.2.dsc.config.yaml +``` + +```yaml +results: +- name: Echo + type: Microsoft.DSC.Debug/Echo + result: + actualState: + output: 5 +messages: [] +hadErrors: false +``` + +### Example 3 - Parse nested JSON structure + +This example parses a JSON string with nested objects and arrays, then accesses nested +properties using array indexing and property access. + +```yaml +# json.example.3.dsc.config.yaml +$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json +resources: +- name: Echo + type: Microsoft.DSC.Debug/Echo + properties: + output: "[json('{\"users\":[{\"name\":\"Alice\"},{\"name\":\"Bob\"}]}').users[0].name]" +``` + +```bash +dsc config get --file json.example.3.dsc.config.yaml +``` + +```yaml +results: +- name: Echo + type: Microsoft.DSC.Debug/Echo + result: + actualState: + output: Alice +messages: [] +hadErrors: false +``` + +### Example 4 - Parse JSON with whitespace + +This example shows that `json()` handles JSON strings with extra whitespace. + +```yaml +# json.example.4.dsc.config.yaml +$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json +resources: +- name: Echo + type: Microsoft.DSC.Debug/Echo + properties: + output: "[json(' { \"key\" : \"value\" } ').key]" +``` + +```bash +dsc config get --file json.example.4.dsc.config.yaml +``` + +```yaml +results: +- name: Echo + type: Microsoft.DSC.Debug/Echo + result: + actualState: + output: value +messages: [] +hadErrors: false +``` + +### Example 5 - Parse primitive JSON values + +This example demonstrates parsing different primitive JSON values including strings, +numbers, and booleans. + +```yaml +# json.example.5.dsc.config.yaml +$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json +resources: +- name: Echo string + type: Microsoft.DSC.Debug/Echo + properties: + output: "[json('\"hello\"')]" +- name: Echo number + type: Microsoft.DSC.Debug/Echo + properties: + output: "[json('42')]" +- name: Echo boolean + type: Microsoft.DSC.Debug/Echo + properties: + output: "[json('true')]" +``` + +```bash +dsc config get --file json.example.5.dsc.config.yaml +``` + +```yaml +results: +- name: Echo string + type: Microsoft.DSC.Debug/Echo + result: + actualState: + output: hello +- name: Echo number + type: Microsoft.DSC.Debug/Echo + result: + actualState: + output: 42 +- name: Echo boolean + type: Microsoft.DSC.Debug/Echo + result: + actualState: + output: true +messages: [] +hadErrors: false +``` + +## Parameters + +### arg1 + +The JSON string to parse. Must be a properly formatted JSON string. + +```yaml +Type: string +Required: true +Position: 1 +``` + +## Output + +Returns the parsed JSON value. The type depends on the JSON content: + +- Object for JSON objects +- Array for JSON arrays +- String for JSON strings +- Number for JSON numbers +- Boolean for JSON booleans +- Null for JSON null + +```yaml +Type: object | array | string | number | boolean | null +``` + +## Related functions + +- [`length()`][00] - Returns the length of an array or object +- [`string()`][01] - Converts values to strings + + +[00]: ./length.md +[01]: ./string.md diff --git a/dsc/tests/dsc_functions.tests.ps1 b/dsc/tests/dsc_functions.tests.ps1 index 5149c40b5..0af70c684 100644 --- a/dsc/tests/dsc_functions.tests.ps1 +++ b/dsc/tests/dsc_functions.tests.ps1 @@ -1060,4 +1060,60 @@ Describe 'tests for function expressions' { $out = $config_yaml | dsc config get -f - | ConvertFrom-Json $out.results[0].result.actualState.output | Should -BeExactly $expected } + + It 'json() works: ' -TestCases @( + @{ data = @{ name = 'John'; age = 30 }; accessor = '.name'; expected = 'John' } + @{ data = @{ name = 'John'; age = 30 }; accessor = '.age'; expected = 30 } + @{ data = @(1,2,3); accessor = '[0]'; expected = 1 } + @{ data = @(1,2,3); accessor = '[2]'; expected = 3 } + @{ data = 'hello'; accessor = ''; expected = 'hello' } + @{ data = 42; accessor = ''; expected = 42 } + @{ data = $true; accessor = ''; expected = $true } + @{ data = $false; accessor = ''; expected = $false } + @{ data = $null; accessor = ''; expected = $null } + @{ data = @{ users = @( @{ name = 'Alice' }, @{ name = 'Bob' } ) }; accessor = '.users[0].name'; expected = 'Alice' } + @{ data = @{ users = @( @{ name = 'Alice' }, @{ name = 'Bob' } ) }; accessor = '.users[1].name'; expected = 'Bob' } + @{ data = @{ key = 'value' }; accessor = '.key'; expected = 'value' } + @{ data = @{ nested = @{ value = 123 } }; accessor = '.nested.value'; expected = 123 } + ) { + param($data, $accessor, $expected) + + $jsonString = ConvertTo-Json -Compress -InputObject $data + $expression = "[json(''$($jsonString)'')$accessor]" + + $config_yaml = @" + `$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json + resources: + - name: Echo + type: Microsoft.DSC.Debug/Echo + properties: + output: '$expression' +"@ + $out = $config_yaml | dsc config get -f - | ConvertFrom-Json + $out.results[0].result.actualState.output | Should -Be $expected + } + + It 'json() error handling: ' -TestCases @( + @{ expression = "[json('not valid json')]" } + @{ expression = "[json('{""key"":""value""')]" } + @{ expression = "[json('')]" } + @{ expression = "[json('{incomplete')]" } + @{ expression = "[json('[1,2,')]" } + ) { + param($expression) + + $escapedExpression = $expression -replace "'", "''" + $config_yaml = @" + `$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json + resources: + - name: Echo + type: Microsoft.DSC.Debug/Echo + properties: + output: '$escapedExpression' +"@ + $null = dsc -l trace config get -i $config_yaml 2>$TestDrive/error.log + $LASTEXITCODE | Should -Not -Be 0 + $errorContent = Get-Content $TestDrive/error.log -Raw + $errorContent | Should -Match ([regex]::Escape('Invalid JSON string')) + } } diff --git a/lib/dsc-lib/locales/en-us.toml b/lib/dsc-lib/locales/en-us.toml index 6fe75107d..9c1a71628 100644 --- a/lib/dsc-lib/locales/en-us.toml +++ b/lib/dsc-lib/locales/en-us.toml @@ -384,6 +384,10 @@ invalidNullElement = "Array elements cannot be null" invalidArrayElement = "Array elements cannot be arrays" invalidObjectElement = "Array elements cannot be objects" +[functions.json] +description = "Converts a valid JSON string into a JSON data type" +invalidJson = "Invalid JSON string" + [functions.lastIndexOf] description = "Returns the index of the last occurrence of an item in an array" invoked = "lastIndexOf function" diff --git a/lib/dsc-lib/src/functions/json.rs b/lib/dsc-lib/src/functions/json.rs new file mode 100644 index 000000000..be7d35f86 --- /dev/null +++ b/lib/dsc-lib/src/functions/json.rs @@ -0,0 +1,152 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +use crate::DscError; +use crate::configure::context::Context; +use crate::functions::{FunctionArgKind, Function, FunctionCategory, FunctionMetadata}; +use rust_i18n::t; +use serde_json::Value; + +#[derive(Debug, Default)] +pub struct Json {} + +impl Function for Json { + fn get_metadata(&self) -> FunctionMetadata { + FunctionMetadata { + name: "json".to_string(), + description: t!("functions.json.description").to_string(), + category: vec![FunctionCategory::Object], + min_args: 1, + max_args: 1, + accepted_arg_ordered_types: vec![ + vec![FunctionArgKind::String], + ], + remaining_arg_accepted_types: None, + return_types: vec![FunctionArgKind::Object, FunctionArgKind::Array, FunctionArgKind::String, FunctionArgKind::Number, FunctionArgKind::Boolean], + } + } + + fn invoke(&self, args: &[Value], _context: &Context) -> Result { + let json_str = args[0].as_str().unwrap(); + + match serde_json::from_str(json_str) { + Ok(value) => Ok(value), + Err(e) => Err(DscError::Parser(format!("{}: {}", t!("functions.json.invalidJson"), e))), + } + } +} + +#[cfg(test)] +mod tests { + use crate::configure::context::Context; + use crate::parser::Statement; + use serde_json::json; + + #[test] + fn json_parse_object() { + let mut parser = Statement::new().unwrap(); + let result = parser.parse_and_execute(r#"[json('{"name":"John","age":30}')]"#, &Context::new()).unwrap(); + + assert!(result.is_object()); + let obj = result.as_object().unwrap(); + assert_eq!(obj.get("name").and_then(|v| v.as_str()), Some("John")); + assert_eq!(obj.get("age").and_then(|v| v.as_i64()), Some(30)); + } + + #[test] + fn json_parse_array() { + let mut parser = Statement::new().unwrap(); + let result = parser.parse_and_execute(r#"[json('[1,2,3]')]"#, &Context::new()).unwrap(); + + assert_eq!(result, json!([1, 2, 3])); + } + + #[test] + fn json_parse_string() { + let mut parser = Statement::new().unwrap(); + let result = parser.parse_and_execute(r#"[json('"hello"')]"#, &Context::new()).unwrap(); + + assert_eq!(result, json!("hello")); + } + + #[test] + fn json_parse_number() { + let mut parser = Statement::new().unwrap(); + let result = parser.parse_and_execute(r#"[json('42')]"#, &Context::new()).unwrap(); + + assert_eq!(result, json!(42)); + } + + #[test] + fn json_parse_boolean() { + let mut parser = Statement::new().unwrap(); + let result = parser.parse_and_execute(r#"[json('true')]"#, &Context::new()).unwrap(); + + assert_eq!(result, json!(true)); + } + + #[test] + fn json_parse_null() { + let mut parser = Statement::new().unwrap(); + let result = parser.parse_and_execute(r#"[json('null')]"#, &Context::new()).unwrap(); + + assert_eq!(result, json!(null)); + } + + #[test] + fn json_parse_nested_object() { + let mut parser = Statement::new().unwrap(); + let result = parser.parse_and_execute(r#"[json('{"user":{"name":"Jane","roles":["admin","user"]}}')]"#, &Context::new()).unwrap(); + + assert!(result.is_object()); + let obj = result.as_object().unwrap(); + let user = obj.get("user").unwrap().as_object().unwrap(); + assert_eq!(user.get("name").and_then(|v| v.as_str()), Some("Jane")); + + let roles = user.get("roles").unwrap().as_array().unwrap(); + assert_eq!(roles.len(), 2); + assert_eq!(roles[0].as_str(), Some("admin")); + } + + #[test] + fn json_parse_with_whitespace() { + let mut parser = Statement::new().unwrap(); + let result = parser.parse_and_execute(r#"[json(' { "key" : "value" } ')]"#, &Context::new()).unwrap(); + + assert!(result.is_object()); + let obj = result.as_object().unwrap(); + assert_eq!(obj.get("key").and_then(|v| v.as_str()), Some("value")); + } + + #[test] + fn json_invalid_string_error() { + let mut parser = Statement::new().unwrap(); + let result = parser.parse_and_execute(r#"[json('not valid json')]"#, &Context::new()); + + assert!(result.is_err()); + } + + #[test] + fn json_unclosed_brace_error() { + let mut parser = Statement::new().unwrap(); + let result = parser.parse_and_execute(r#"[json('{"key":"value"')]"#, &Context::new()); + + assert!(result.is_err()); + } + + #[test] + fn json_empty_string_error() { + let mut parser = Statement::new().unwrap(); + let result = parser.parse_and_execute(r#"[json('')]"#, &Context::new()); + + assert!(result.is_err()); + } + + #[test] + fn json_not_string_error() { + let mut parser = Statement::new().unwrap(); + let result = parser.parse_and_execute(r#"[json(123)]"#, &Context::new()); + + assert!(result.is_err()); + } +} diff --git a/lib/dsc-lib/src/functions/mod.rs b/lib/dsc-lib/src/functions/mod.rs index dae8e9578..9e915f221 100644 --- a/lib/dsc-lib/src/functions/mod.rs +++ b/lib/dsc-lib/src/functions/mod.rs @@ -44,6 +44,7 @@ pub mod index_of; pub mod intersection; pub mod items; pub mod join; +pub mod json; pub mod last_index_of; pub mod max; pub mod min; @@ -170,6 +171,7 @@ impl FunctionDispatcher { Box::new(intersection::Intersection{}), Box::new(items::Items{}), Box::new(join::Join{}), + Box::new(json::Json{}), Box::new(last_index_of::LastIndexOf{}), Box::new(max::Max{}), Box::new(min::Min{}),