This is an automated email from the ASF dual-hosted git repository.
otto pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/nifi.git
The following commit(s) were added to refs/heads/main by this push:
new bc5204d NIFI-8137 Record Path EscapeJson/UnescapeJson functions
(#4756)
bc5204d is described below
commit bc5204d4df8017cbde5f2d050d466cdb04e7f969
Author: Chris Sampson <[email protected]>
AuthorDate: Fri Jun 4 14:19:24 2021 +0100
NIFI-8137 Record Path EscapeJson/UnescapeJson functions (#4756)
* NIFI-8137 Record Path EscapeJson/UnescapeJson functions
* Correct jackson-databind dependency version
* Add negative tests for RecordPath JSON handling; rename RecordPath JSON
classes to better match existing functions
Signed-off-by: Otto Fowler <[email protected]>
This closes #4756.
---
.../language/compile/ExpressionCompiler.java | 18 +--
nifi-commons/nifi-record-path/pom.xml | 5 +
.../nifi/record/path/functions/JsonEscape.java | 65 ++++++++++
.../nifi/record/path/functions/JsonUnescape.java | 91 ++++++++++++++
.../nifi/record/path/paths/RecordPathCompiler.java | 10 ++
.../apache/nifi/record/path/TestRecordPath.java | 132 +++++++++++++++++++++
nifi-docs/src/main/asciidoc/record-path-guide.adoc | 90 ++++++++++++++
7 files changed, 402 insertions(+), 9 deletions(-)
diff --git
a/nifi-commons/nifi-expression-language/src/main/java/org/apache/nifi/attribute/expression/language/compile/ExpressionCompiler.java
b/nifi-commons/nifi-expression-language/src/main/java/org/apache/nifi/attribute/expression/language/compile/ExpressionCompiler.java
index 4699e60..767928a 100644
---
a/nifi-commons/nifi-expression-language/src/main/java/org/apache/nifi/attribute/expression/language/compile/ExpressionCompiler.java
+++
b/nifi-commons/nifi-expression-language/src/main/java/org/apache/nifi/attribute/expression/language/compile/ExpressionCompiler.java
@@ -599,15 +599,15 @@ public class ExpressionCompiler {
}
case ESCAPE_CSV: {
verifyArgCount(argEvaluators, 0, "escapeCsv");
- return
addToken(CharSequenceTranslatorEvaluator.csvEscapeEvaluator(toStringEvaluator(subjectEvaluator)),
"escapeJson");
+ return
addToken(CharSequenceTranslatorEvaluator.csvEscapeEvaluator(toStringEvaluator(subjectEvaluator)),
"escapeCsv");
}
case ESCAPE_HTML3: {
verifyArgCount(argEvaluators, 0, "escapeHtml3");
- return
addToken(CharSequenceTranslatorEvaluator.html3EscapeEvaluator(toStringEvaluator(subjectEvaluator)),
"escapeJson");
+ return
addToken(CharSequenceTranslatorEvaluator.html3EscapeEvaluator(toStringEvaluator(subjectEvaluator)),
"escapeHtml3");
}
case ESCAPE_HTML4: {
verifyArgCount(argEvaluators, 0, "escapeHtml4");
- return
addToken(CharSequenceTranslatorEvaluator.html4EscapeEvaluator(toStringEvaluator(subjectEvaluator)),
"escapeJson");
+ return
addToken(CharSequenceTranslatorEvaluator.html4EscapeEvaluator(toStringEvaluator(subjectEvaluator)),
"escapeHtml4");
}
case ESCAPE_JSON: {
verifyArgCount(argEvaluators, 0, "escapeJson");
@@ -615,27 +615,27 @@ public class ExpressionCompiler {
}
case ESCAPE_XML: {
verifyArgCount(argEvaluators, 0, "escapeXml");
- return
addToken(CharSequenceTranslatorEvaluator.xmlEscapeEvaluator(toStringEvaluator(subjectEvaluator)),
"escapeJson");
+ return
addToken(CharSequenceTranslatorEvaluator.xmlEscapeEvaluator(toStringEvaluator(subjectEvaluator)),
"escapeXml");
}
case UNESCAPE_CSV: {
verifyArgCount(argEvaluators, 0, "unescapeCsv");
- return
addToken(CharSequenceTranslatorEvaluator.csvUnescapeEvaluator(toStringEvaluator(subjectEvaluator)),
"escapeJson");
+ return
addToken(CharSequenceTranslatorEvaluator.csvUnescapeEvaluator(toStringEvaluator(subjectEvaluator)),
"unescapeCsv");
}
case UNESCAPE_HTML3: {
verifyArgCount(argEvaluators, 0, "unescapeHtml3");
- return
addToken(CharSequenceTranslatorEvaluator.html3UnescapeEvaluator(toStringEvaluator(subjectEvaluator)),
"escapeJson");
+ return
addToken(CharSequenceTranslatorEvaluator.html3UnescapeEvaluator(toStringEvaluator(subjectEvaluator)),
"unescapeHtml3");
}
case UNESCAPE_HTML4: {
verifyArgCount(argEvaluators, 0, "unescapeHtml4");
- return
addToken(CharSequenceTranslatorEvaluator.html4UnescapeEvaluator(toStringEvaluator(subjectEvaluator)),
"escapeJson");
+ return
addToken(CharSequenceTranslatorEvaluator.html4UnescapeEvaluator(toStringEvaluator(subjectEvaluator)),
"unescapeHtml4");
}
case UNESCAPE_JSON: {
verifyArgCount(argEvaluators, 0, "unescapeJson");
- return
addToken(CharSequenceTranslatorEvaluator.jsonUnescapeEvaluator(toStringEvaluator(subjectEvaluator)),
"escapeJson");
+ return
addToken(CharSequenceTranslatorEvaluator.jsonUnescapeEvaluator(toStringEvaluator(subjectEvaluator)),
"unescapeJson");
}
case UNESCAPE_XML: {
verifyArgCount(argEvaluators, 0, "unescapeXml");
- return
addToken(CharSequenceTranslatorEvaluator.xmlUnescapeEvaluator(toStringEvaluator(subjectEvaluator)),
"escapeJson");
+ return
addToken(CharSequenceTranslatorEvaluator.xmlUnescapeEvaluator(toStringEvaluator(subjectEvaluator)),
"unescapeXml");
}
case SUBSTRING_BEFORE: {
verifyArgCount(argEvaluators, 1, "substringBefore");
diff --git a/nifi-commons/nifi-record-path/pom.xml
b/nifi-commons/nifi-record-path/pom.xml
index b4658d6..334eeeb 100644
--- a/nifi-commons/nifi-record-path/pom.xml
+++ b/nifi-commons/nifi-record-path/pom.xml
@@ -91,5 +91,10 @@
<artifactId>commons-codec</artifactId>
<version>1.14</version>
</dependency>
+ <dependency>
+ <groupId>com.fasterxml.jackson.core</groupId>
+ <artifactId>jackson-databind</artifactId>
+ <version>${jackson.version}</version>
+ </dependency>
</dependencies>
</project>
diff --git
a/nifi-commons/nifi-record-path/src/main/java/org/apache/nifi/record/path/functions/JsonEscape.java
b/nifi-commons/nifi-record-path/src/main/java/org/apache/nifi/record/path/functions/JsonEscape.java
new file mode 100644
index 0000000..b452c6c
--- /dev/null
+++
b/nifi-commons/nifi-record-path/src/main/java/org/apache/nifi/record/path/functions/JsonEscape.java
@@ -0,0 +1,65 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.nifi.record.path.functions;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.apache.nifi.record.path.FieldValue;
+import org.apache.nifi.record.path.RecordPathEvaluationContext;
+import org.apache.nifi.record.path.StandardFieldValue;
+import org.apache.nifi.record.path.exception.RecordPathException;
+import org.apache.nifi.record.path.paths.RecordPathSegment;
+import org.apache.nifi.serialization.record.Record;
+import org.apache.nifi.serialization.record.RecordFieldType;
+import org.apache.nifi.serialization.record.util.DataTypeUtils;
+
+import java.util.stream.Stream;
+
+public class JsonEscape extends RecordPathSegment {
+ private final RecordPathSegment recordPath;
+
+ private final ObjectMapper objectMapper = new ObjectMapper();
+
+ public JsonEscape(final RecordPathSegment recordPath, final boolean
absolute) {
+ super("jsonEscape", null, absolute);
+ this.recordPath = recordPath;
+ }
+
+ @Override
+ public Stream<FieldValue> evaluate(final RecordPathEvaluationContext
context) {
+ final Stream<FieldValue> fieldValues = recordPath.evaluate(context);
+ return fieldValues.filter(fv -> fv.getValue() != null)
+ .map(fv -> {
+ Object value = fv.getValue();
+ if (value == null) {
+ return new StandardFieldValue(null, fv.getField(),
fv.getParent().orElse(null));
+ } else {
+ if (value instanceof Record) {
+ value =
DataTypeUtils.convertRecordFieldtoObject(value,
RecordFieldType.RECORD.getDataType());
+ }
+
+ try {
+ return new
StandardFieldValue(objectMapper.writeValueAsString(value), fv.getField(),
fv.getParent().orElse(null));
+ } catch (JsonProcessingException e) {
+ throw new RecordPathException("Unable to serialise
Record Path value as JSON String", e);
+ }
+ }
+ });
+ }
+
+}
diff --git
a/nifi-commons/nifi-record-path/src/main/java/org/apache/nifi/record/path/functions/JsonUnescape.java
b/nifi-commons/nifi-record-path/src/main/java/org/apache/nifi/record/path/functions/JsonUnescape.java
new file mode 100644
index 0000000..d16a280
--- /dev/null
+++
b/nifi-commons/nifi-record-path/src/main/java/org/apache/nifi/record/path/functions/JsonUnescape.java
@@ -0,0 +1,91 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.nifi.record.path.functions;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.apache.nifi.record.path.FieldValue;
+import org.apache.nifi.record.path.RecordPathEvaluationContext;
+import org.apache.nifi.record.path.StandardFieldValue;
+import org.apache.nifi.record.path.exception.RecordPathException;
+import org.apache.nifi.record.path.paths.RecordPathSegment;
+import org.apache.nifi.serialization.record.DataType;
+import org.apache.nifi.serialization.record.type.ArrayDataType;
+import org.apache.nifi.serialization.record.type.ChoiceDataType;
+import org.apache.nifi.serialization.record.type.RecordDataType;
+import org.apache.nifi.serialization.record.util.DataTypeUtils;
+
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.Map;
+import java.util.stream.Stream;
+
+public class JsonUnescape extends RecordPathSegment {
+ private final RecordPathSegment recordPath;
+
+ private final ObjectMapper objectMapper = new ObjectMapper();
+
+ public JsonUnescape(final RecordPathSegment recordPath, final boolean
absolute) {
+ super("jsonUnescape", null, absolute);
+ this.recordPath = recordPath;
+ }
+
+ @Override
+ public Stream<FieldValue> evaluate(final RecordPathEvaluationContext
context) {
+ final Stream<FieldValue> fieldValues = recordPath.evaluate(context);
+ return fieldValues.filter(fv -> fv.getValue() != null)
+ .map(fv -> {
+ Object value = fv.getValue();
+
+ if (value instanceof String) {
+ try {
+ DataType dataType = fv.getField().getDataType();
+ if (fv.getField().getDataType() instanceof
ChoiceDataType) {
+ dataType = DataTypeUtils.chooseDataType(value,
(ChoiceDataType) fv.getField().getDataType());
+ }
+
+ return new
StandardFieldValue(convertFieldValue(value, fv.getField().getFieldName(),
dataType), fv.getField(), fv.getParent().orElse(null));
+ } catch (IOException e) {
+ throw new RecordPathException("Unable to
deserialise JSON String into Record Path value", e);
+ }
+ } else {
+ throw new IllegalArgumentException("Argument supplied
to jsonUnescape must be a String");
+ }
+ });
+ }
+
+ @SuppressWarnings("unchecked")
+ private Object convertFieldValue(final Object value, final String
fieldName, final DataType dataType) throws IOException {
+ if (dataType instanceof RecordDataType) {
+ // convert Maps to Records
+ final Map<String, Object> map =
objectMapper.readValue(value.toString(), Map.class);
+ return DataTypeUtils.toRecord(map, ((RecordDataType)
dataType).getChildSchema(), fieldName);
+ } else if (dataType instanceof ArrayDataType) {
+ final DataType elementDataType = ((ArrayDataType)
dataType).getElementType();
+
+ // convert Arrays of Maps to Records
+ Object[] arr = objectMapper.readValue(value.toString(),
Object[].class);
+ if (elementDataType instanceof RecordDataType) {
+ arr = Arrays.stream(arr).map(e -> DataTypeUtils.toRecord(e,
((RecordDataType) elementDataType).getChildSchema(), fieldName)).toArray();
+ }
+ return arr;
+ } else {
+ // generic conversion for simpler fields
+ return objectMapper.readValue(value.toString(), Object.class);
+ }
+ }
+}
diff --git
a/nifi-commons/nifi-record-path/src/main/java/org/apache/nifi/record/path/paths/RecordPathCompiler.java
b/nifi-commons/nifi-record-path/src/main/java/org/apache/nifi/record/path/paths/RecordPathCompiler.java
index 7cb1ead..cd46a40 100644
---
a/nifi-commons/nifi-record-path/src/main/java/org/apache/nifi/record/path/paths/RecordPathCompiler.java
+++
b/nifi-commons/nifi-record-path/src/main/java/org/apache/nifi/record/path/paths/RecordPathCompiler.java
@@ -39,6 +39,7 @@ import org.apache.nifi.record.path.functions.Base64Decode;
import org.apache.nifi.record.path.functions.Base64Encode;
import org.apache.nifi.record.path.functions.Coalesce;
import org.apache.nifi.record.path.functions.Concat;
+import org.apache.nifi.record.path.functions.JsonEscape;
import org.apache.nifi.record.path.functions.FieldName;
import org.apache.nifi.record.path.functions.Format;
import org.apache.nifi.record.path.functions.Hash;
@@ -59,6 +60,7 @@ import org.apache.nifi.record.path.functions.ToString;
import org.apache.nifi.record.path.functions.ToUpperCase;
import org.apache.nifi.record.path.functions.TrimString;
import org.apache.nifi.record.path.functions.UUID5;
+import org.apache.nifi.record.path.functions.JsonUnescape;
import java.util.ArrayList;
import java.util.List;
@@ -308,6 +310,14 @@ public class RecordPathCompiler {
final RecordPathSegment[] args =
getArgPaths(argumentListTree, 1, functionName, absolute);
return new Base64Decode(args[0], absolute);
}
+ case "jsonEscape": {
+ final RecordPathSegment[] args =
getArgPaths(argumentListTree, 1, functionName, absolute);
+ return new JsonEscape(args[0], absolute);
+ }
+ case "jsonUnescape": {
+ final RecordPathSegment[] args =
getArgPaths(argumentListTree, 1, functionName, absolute);
+ return new JsonUnescape(args[0], absolute);
+ }
case "hash":{
final RecordPathSegment[] args =
getArgPaths(argumentListTree, 2, functionName, absolute);
return new Hash(args[0], args[1], absolute);
diff --git
a/nifi-commons/nifi-record-path/src/test/java/org/apache/nifi/record/path/TestRecordPath.java
b/nifi-commons/nifi-record-path/src/test/java/org/apache/nifi/record/path/TestRecordPath.java
index 682ab7f..f5a105d 100644
---
a/nifi-commons/nifi-record-path/src/test/java/org/apache/nifi/record/path/TestRecordPath.java
+++
b/nifi-commons/nifi-record-path/src/test/java/org/apache/nifi/record/path/TestRecordPath.java
@@ -38,6 +38,7 @@ import java.text.ParseException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Base64;
+import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@@ -51,6 +52,7 @@ import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
public class TestRecordPath {
@@ -67,6 +69,7 @@ public class TestRecordPath {
// substring is not a filter function so cannot be used as a predicate
try {
RecordPath.compile("/name[substring(., 1, 2)]");
+ fail("Expected RecordPathException");
} catch (final RecordPathException e) {
// expected
}
@@ -1644,6 +1647,135 @@ public class TestRecordPath {
}
@Test
+ public void testJsonEscape() {
+ final RecordSchema address = new
SimpleRecordSchema(Collections.singletonList(
+ new RecordField("address_1",
RecordFieldType.STRING.getDataType())
+ ));
+
+ final RecordSchema person = new SimpleRecordSchema(Arrays.asList(
+ new RecordField("firstName",
RecordFieldType.STRING.getDataType()),
+ new RecordField("age", RecordFieldType.INT.getDataType()),
+ new RecordField("nicknames",
RecordFieldType.ARRAY.getArrayDataType(RecordFieldType.STRING.getDataType())),
+ new RecordField("addresses",
RecordFieldType.ARRAY.getArrayDataType(RecordFieldType.RECORD.getRecordDataType(address)))
+ ));
+
+ final RecordSchema schema = new
SimpleRecordSchema(Collections.singletonList(
+ new RecordField("person",
RecordFieldType.RECORD.getRecordDataType(person))
+ ));
+
+ final Map<String, Object> values = new HashMap<String, Object>(){{
+ put("person", new MapRecord(person, new HashMap<String, Object>(){{
+ put("firstName", "John");
+ put("age", 30);
+ put("nicknames", new String[] {"J", "Johnny"});
+ put("addresses", new MapRecord[]{
+ new MapRecord(address,
Collections.singletonMap("address_1", "123 Somewhere Street")),
+ new MapRecord(address,
Collections.singletonMap("address_1", "456 Anywhere Road"))
+ });
+ }}));
+ }};
+
+ final Record record = new MapRecord(schema, values);
+
+ assertEquals("\"John\"",
RecordPath.compile("jsonEscape(/person/firstName)").evaluate(record).getSelectedFields().findFirst().orElseThrow(IllegalStateException::new).getValue());
+ assertEquals("30",
RecordPath.compile("jsonEscape(/person/age)").evaluate(record).getSelectedFields().findFirst().orElseThrow(IllegalStateException::new).getValue());
+ assertEquals(
+
"{\"firstName\":\"John\",\"age\":30,\"nicknames\":[\"J\",\"Johnny\"],\"addresses\":[{\"address_1\":\"123
Somewhere Street\"},{\"address_1\":\"456 Anywhere Road\"}]}",
+
RecordPath.compile("jsonEscape(/person)").evaluate(record).getSelectedFields().findFirst().orElseThrow(IllegalStateException::new).getValue()
+ );
+ }
+
+ @Test
+ public void testJsonUnescape() {
+ final RecordSchema address = new
SimpleRecordSchema(Collections.singletonList(
+ new RecordField("address_1",
RecordFieldType.STRING.getDataType())
+ ));
+
+ final RecordSchema person = new SimpleRecordSchema(Arrays.asList(
+ new RecordField("firstName",
RecordFieldType.STRING.getDataType()),
+ new RecordField("age", RecordFieldType.INT.getDataType()),
+ new RecordField("nicknames",
RecordFieldType.ARRAY.getArrayDataType(RecordFieldType.STRING.getDataType())),
+ new RecordField("addresses",
RecordFieldType.CHOICE.getChoiceDataType(
+
RecordFieldType.ARRAY.getArrayDataType(RecordFieldType.RECORD.getRecordDataType(address)),
+ RecordFieldType.RECORD.getRecordDataType(address)
+ ))
+ ));
+
+ final RecordSchema schema = new SimpleRecordSchema(Arrays.asList(
+ new RecordField("person",
RecordFieldType.RECORD.getRecordDataType(person)),
+ new RecordField("json_str",
RecordFieldType.STRING.getDataType())
+ ));
+
+ // test CHOICE resulting in nested ARRAY of RECORDs
+ final Record recordAddressesArray = new MapRecord(schema,
+ Collections.singletonMap(
+ "json_str",
+
"{\"firstName\":\"John\",\"age\":30,\"nicknames\":[\"J\",\"Johnny\"],\"addresses\":[{\"address_1\":\"123
Somewhere Street\"},{\"address_1\":\"456 Anywhere Road\"}]}")
+ );
+ assertEquals(
+ new HashMap<String, Object>(){{
+ put("firstName", "John");
+ put("age", 30);
+ put("nicknames", Arrays.asList("J", "Johnny"));
+ put("addresses", Arrays.asList(
+ Collections.singletonMap("address_1", "123
Somewhere Street"),
+ Collections.singletonMap("address_1", "456
Anywhere Road")
+ ));
+ }},
+
RecordPath.compile("jsonUnescape(/json_str)").evaluate(recordAddressesArray).getSelectedFields().findFirst().orElseThrow(IllegalStateException::new).getValue()
+ );
+
+ // test CHOICE resulting in nested single RECORD
+ final Record recordAddressesSingle = new MapRecord(schema,
+ Collections.singletonMap(
+ "json_str",
+
"{\"firstName\":\"John\",\"age\":30,\"nicknames\":[\"J\",\"Johnny\"],\"addresses\":{\"address_1\":\"123
Somewhere Street\"}}")
+ );
+ assertEquals(
+ new HashMap<String, Object>(){{
+ put("firstName", "John");
+ put("age", 30);
+ put("nicknames", Arrays.asList("J", "Johnny"));
+ put("addresses", Collections.singletonMap("address_1",
"123 Somewhere Street"));
+ }},
+
RecordPath.compile("jsonUnescape(/json_str)").evaluate(recordAddressesSingle).getSelectedFields().findFirst().orElseThrow(IllegalStateException::new).getValue()
+ );
+
+ // test simple String field
+ final Record recordJustName = new MapRecord(schema,
Collections.singletonMap("json_str", "{\"firstName\":\"John\"}"));
+ assertEquals(
+ new HashMap<String, Object>(){{put("firstName", "John");}},
+
RecordPath.compile("jsonUnescape(/json_str)").evaluate(recordJustName).getSelectedFields().findFirst().orElseThrow(IllegalStateException::new).getValue()
+ );
+
+ // test simple String
+ final Record recordJustString = new MapRecord(schema,
Collections.singletonMap("json_str", "\"John\""));
+ assertEquals("John",
RecordPath.compile("jsonUnescape(/json_str)").evaluate(recordJustString).getSelectedFields().findFirst().orElseThrow(IllegalStateException::new).getValue());
+
+ // test simple Int
+ final Record recordJustInt = new MapRecord(schema,
Collections.singletonMap("json_str", "30"));
+ assertEquals(30,
RecordPath.compile("jsonUnescape(/json_str)").evaluate(recordJustInt).getSelectedFields().findFirst().orElseThrow(IllegalStateException::new).getValue());
+
+ // test invalid JSON
+ final Record recordInvalidJson = new MapRecord(schema,
Collections.singletonMap("json_str", "{\"invalid\": \"json"));
+ try {
+
RecordPath.compile("jsonUnescape(/json_str)").evaluate(recordInvalidJson).getSelectedFields().findFirst().orElseThrow(IllegalStateException::new).getValue();
+ fail("Expected a RecordPathException for invalid JSON");
+ } catch (RecordPathException rpe) {
+ assertEquals("Unable to deserialise JSON String into Record Path
value", rpe.getMessage());
+ }
+
+ // test not String
+ final Record recordNotString = new MapRecord(schema,
Collections.singletonMap("person", new MapRecord(person,
Collections.singletonMap("age", 30))));
+ try {
+
RecordPath.compile("jsonUnescape(/person/age)").evaluate(recordNotString).getSelectedFields().findFirst().orElseThrow(IllegalStateException::new).getValue();
+ fail("Expected IllegalArgumentException for non-String input");
+ } catch (IllegalArgumentException iae) {
+ assertEquals("Argument supplied to jsonUnescape must be a String",
iae.getMessage());
+ }
+ }
+
+ @Test
public void testHash() {
final Record record = getCaseTestRecord();
assertEquals("61409aa1fd47d4a5332de23cbf59a36f",
RecordPath.compile("hash(/firstName,
'MD5')").evaluate(record).getSelectedFields().findFirst().get().getValue());
diff --git a/nifi-docs/src/main/asciidoc/record-path-guide.adoc
b/nifi-docs/src/main/asciidoc/record-path-guide.adoc
index 0f2f2dc..f75c2b5 100644
--- a/nifi-docs/src/main/asciidoc/record-path-guide.adoc
+++ b/nifi-docs/src/main/asciidoc/record-path-guide.adoc
@@ -851,6 +851,96 @@ The following record path expression would decode the
String using Base64:
| `base64Decode(/name)` | John
|==========================================================
+=== jsonEscape
+
+JSON Stringifies a Record, Array or simple field (e.g. String), using the
UTF-8 character set. For example, given a schema such as:
+
+----
+{
+ "type": "record",
+ "name": "events",
+ "fields": [{
+ "name": "person",
+ "type": "record",
+ "fields": [
+ { "name": "name", "type": "string" },
+ { "name": "age", "type": "int" }
+ ]
+ }]
+}
+----
+
+and a record such as:
+
+----
+{
+ "person": {
+ "name" : "John",
+ "age" : 30
+ }
+}
+----
+
+The following record path expression would convert the record into an escaped
JSON String:
+
+|==========================================================
+| RecordPath | Return value
+| `jsonEscape(/person)` | "{\"person\":{\"name\":\"John\",\"age\":30}}"
+| `jsonEscape(/person/firstName)` | "\"John\""
+| `jsonEscape(/person/age)` | "30"
+|==========================================================
+
+=== jsonUnescape
+
+Converts a stringified JSON element to a Record, Array or simple field (e.g.
String), using the UTF-8 character set. For example, given a schema such as:
+
+----
+{
+ "type": "record",
+ "name": "events",
+ "fields": [{
+ "name": "person",
+ "type": "record",
+ "fields": [
+ { "name": "name", "type": "string" },
+ { "name": "age", "type": "int" }
+ ]
+ }]
+}
+----
+
+and a record such as:
+
+----
+{
+ "json_str": "{\"person\":{\"name\":\"John\",\"age\":30}}"
+}
+----
+
+The following record path expression would populate the record with unescaped
JSON fields:
+
+|==========================================================
+| RecordPath | Return value
+| `jsonUnescape(/json_str)` | {"person": {"name": "John", "age": 30}}"
+|==========================================================
+
+Given a record such as:
+
+----
+{
+ "json_str": "\"John\""
+}
+----
+
+The following record path expression would return:
+
+|==========================================================
+| RecordPath | Return value
+| `jsonUnescape(/json_str)` | "John"
+|==========================================================
+
+Note that the target schema must be pre-defined if the unescaped JSON is to be
set in a Record's fields - Infer Schema will not currently do this
automatically.
+
=== hash
Converts a String using a hash algorithm. For example, given a schema such as: