diff --git a/ballerina/Ballerina.toml b/ballerina/Ballerina.toml index 0224a1a9..7c34dced 100644 --- a/ballerina/Ballerina.toml +++ b/ballerina/Ballerina.toml @@ -1,13 +1,13 @@ [package] org = "ballerina" name = "xmldata" -version = "2.8.1" +version = "2.9.0" authors = ["Ballerina"] keywords = ["xml", "json"] repository = "https://github.com/ballerina-platform/module-ballerina-xmldata" icon = "icon.png" license = ["Apache-2.0"] -distribution = "2201.10.0" +distribution = "2201.11.0" [platform.java21] graalvmCompatible = true @@ -15,5 +15,5 @@ graalvmCompatible = true [[platform.java21.dependency]] groupId = "io.ballerina.stdlib" artifactId = "xmldata-native" -version = "2.8.1" +version = "2.9.0" path = "../native/build/libs/xmldata-native-2.9.0-SNAPSHOT.jar" diff --git a/ballerina/Dependencies.toml b/ballerina/Dependencies.toml index d406f0d1..c60813a6 100644 --- a/ballerina/Dependencies.toml +++ b/ballerina/Dependencies.toml @@ -5,7 +5,7 @@ [ballerina] dependencies-toml-version = "2" -distribution-version = "2201.11.0-20241112-214900-6b80ab87" +distribution-version = "2201.11.0-20241209-162400-0c015833" [[package]] org = "ballerina" @@ -67,7 +67,7 @@ modules = [ [[package]] org = "ballerina" name = "xmldata" -version = "2.8.1" +version = "2.9.0" dependencies = [ {org = "ballerina", name = "jballerina.java"}, {org = "ballerina", name = "test"} diff --git a/ballerina/tests/from_xml_test.bal b/ballerina/tests/from_xml_test.bal index 276a2485..cfe61828 100644 --- a/ballerina/tests/from_xml_test.bal +++ b/ballerina/tests/from_xml_test.bal @@ -1294,3 +1294,65 @@ isolated function testFromXmlWithNameAnnotation() returns error? { Appointments result = check fromXml(xmlPayload); test:assertEquals(result, expected, msg = "testFromXmlWithNameAnnotation result incorrect"); } + +@Name { + value: "book" +} +type Book record {| + string name; + BookDetails details; +|}; + +type BookDetails record {| + string? author; + string language; +|}; + +@test:Config { + groups: ["fromXml"] +} +function testFromXmlWithNilElementAndWithoutPreserveNS() returns Error? { + xml x1 = xml `Sherlock Holmes +
+ + English +
`; + Book expected = { + name: "Sherlock Holmes", + details: { + author: (), + language: "English" + } + }; + Book result = check fromXml(x1); + test:assertEquals(result, expected, msg = "testFromXmlWithNilElement result incorrect"); +} + +@Name { + value: "book" +} +type Book1 record {| + string name; + BookDetails1 details; +|}; + +type BookDetails1 record {| + string author; + string language; +|}; + +@test:Config { + groups: ["fromXml"] +} +function testFromXmlWithNilElementAndWithoutNillableField() returns Error? { + xml x1 = xml `Sherlock Holmes +
+ + English +
`; + Book1|Error result = fromXml(x1); + test:assertTrue(result is error, msg = "testFromXmlWithNilElementAndWithoutNillableField result incorrect"); + if (result is error) { + test:assertTrue(result.message().includes("field 'details.author' in record 'xmldata:BookDetails1' should be of type 'string', found '()'")); + } +} diff --git a/ballerina/tests/xml_from_json_test.bal b/ballerina/tests/xml_from_json_test.bal index eb1ff332..98271dcc 100644 --- a/ballerina/tests/xml_from_json_test.bal +++ b/ballerina/tests/xml_from_json_test.bal @@ -1096,3 +1096,13 @@ isolated function testWithRootTagConfig() { test:assertFail("failed to convert json to xml"); } } + +@test:Config { + groups: ["fromJson"] +} +function testFromJsonWithNull() returns error? { + json data = null; + xml? result = check fromJson({"name":"Sherlock Holmes", "details":{"author":null, "language":"English"}}, {rootTag: "book"}); + xml expected = xml `Sherlock Holmes
English
`; + test:assertEquals(result, expected, msg = "testFromJsonWithNull result incorrect"); +} diff --git a/ballerina/tests/xml_to_json_test.bal b/ballerina/tests/xml_to_json_test.bal index aaa246af..69d9b5a0 100644 --- a/ballerina/tests/xml_to_json_test.bal +++ b/ballerina/tests/xml_to_json_test.bal @@ -730,3 +730,30 @@ function testToJsonComplexXmlElementWithBackSlash() returns Error? { }; test:assertEquals(j, expectedOutput, msg = "testToJsonComplexXmlElement result incorrect"); } + +@test:Config { + groups: ["toJson"] +} +function testToJsonWithNilElementAndWithoutPreserveNS() returns Error? { + xml x1 = xml `Sherlock Holmes +
+ + English +
`; + json j = check toJson(x1, {preserveNamespaces: false}); + test:assertEquals(j, {"name":"Sherlock Holmes", "details":{"author":null, "language":"English"}}, msg = "testToJsonWithNilElement result incorrect"); +} + +@test:Config { + groups: ["toJson"] +} +function testToJsonWithNilElementAndPreserveNS() returns Error? { + xml x1 = xml `Sherlock Holmes +
+ + English +
`; + json j = check toJson(x1, {preserveNamespaces: true}); + test:assertEquals(j, {"name":"Sherlock Holmes","details":{"author":{"@xsi:nil":"true"},"language":"English", + "@xmlns:xsi":"http://www.w3.org/2001/XMLSchema-instance"}}, msg = "testToJsonWithNilElement result incorrect"); +} diff --git a/changelog.md b/changelog.md index cede5140..5f2ee217 100644 --- a/changelog.md +++ b/changelog.md @@ -6,6 +6,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Fixed +- [Add support for xsi:nil Attribute in xml to json/record conversion](https://github.com/ballerina-platform/ballerina-library/issues/7462) + +## [2.6.2] - 2023-08-17 + ### Fixed [Fix the mismatch error by `fromXml` API while the field has the name annotation](https://github.com/ballerina-platform/ballerina-standard-library/issues/3802) [Make some of the Java classes proper utility classes](https://github.com/ballerina-platform/ballerina-standard-library/issues/4902) diff --git a/native/src/main/java/io/ballerina/stdlib/xmldata/XmlToJson.java b/native/src/main/java/io/ballerina/stdlib/xmldata/XmlToJson.java index 9db49028..9a14bc73 100644 --- a/native/src/main/java/io/ballerina/stdlib/xmldata/XmlToJson.java +++ b/native/src/main/java/io/ballerina/stdlib/xmldata/XmlToJson.java @@ -71,6 +71,8 @@ public class XmlToJson { private static final String EMPTY_STRING = ""; public static final int NS_PREFIX_BEGIN_INDEX = BXmlItem.XMLNS_NS_URI_PREFIX.length(); private static final String COLON = ":"; + public static final String XMLSCHEMA_INSTANCE_NIL = "{http://www.w3.org/2001/XMLSchema-instance}nil"; + public static final BString BSTRING_XMLSCHEMA_INSTANCE_NIL = fromString(XMLSCHEMA_INSTANCE_NIL); /** * Converts an XML to the corresponding JSON representation. @@ -190,7 +192,13 @@ private static Object convertElement(BXmlItem xmlItem, String attributePrefix, children = convertToArray(fieldType, children); } fieldDetail.setParentArrayName(fieldName); - return insertDataToMap(childrenData, children, rootNode, fieldName, fieldType, fieldDetail); + boolean nilValue = false; + // If the children is null and has `{http://www.w3.org/2001/XMLSchema-instance}nil` attribute set to true, + // then it is a nil value + if (children == null && attributeMap.containsKey(BSTRING_XMLSCHEMA_INSTANCE_NIL)) { + nilValue = Boolean.parseBoolean(attributeMap.get(BSTRING_XMLSCHEMA_INSTANCE_NIL).getValue()); + } + return insertDataToMap(childrenData, children, rootNode, fieldName, fieldType, fieldDetail, nilValue); } private static void processAttributeWithAnnotation(BXmlItem xmlItem, String attributePrefix, @@ -224,7 +232,8 @@ private static void processAttributeWithAnnotation(BXmlItem xmlItem, String attr @SuppressWarnings("unchecked") private static BMap insertDataToMap(BMap childrenData, Object children, BMap rootNode, String keyValue, - Type fieldType, FieldDetails fieldDetails) throws Exception { + Type fieldType, FieldDetails fieldDetails, boolean nilValue) + throws Exception { if (childrenData.size() > 0) { if (children instanceof BMap) { BMap data = (BMap) children; @@ -248,9 +257,9 @@ private static BMap insertDataToMap(BMap child put(rootNode, keyValue, children); } else if (children == null) { if (fieldType instanceof ReferenceType && TypeUtils.getReferredType(fieldType) instanceof RecordType) { - put(rootNode, keyValue, ValueCreator.createMapValue(Constants.JSON_MAP_TYPE)); + put(rootNode, keyValue, nilValue ? null : ValueCreator.createMapValue(Constants.JSON_MAP_TYPE)); } else { - putAsFieldTypes(rootNode, keyValue, EMPTY_STRING, fieldType, fieldDetails); + putAsFieldTypes(rootNode, keyValue, nilValue ? null : EMPTY_STRING, fieldType, fieldDetails); } } else if (children instanceof BArray) { put(rootNode, keyValue, children); @@ -645,7 +654,7 @@ private static Object validateResult(Object result, BString elementName) { Object validateResult; if (result == null) { validateResult = fromString(EMPTY_STRING); - } else if (result instanceof BMap && ((BMap) result).get(elementName) != null) { + } else if (result instanceof BMap && ((BMap) result).containsKey(elementName)) { validateResult = ((BMap) result).get(elementName); } else { validateResult = result;