Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[2201.10.x] Support xsi:nil Attribute in xml to json/record conversion #553

Merged
merged 4 commits into from
Dec 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/build-timestamped-master.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,5 +12,5 @@ jobs:
call_workflow:
name: Run Build Workflow
if: ${{ github.repository_owner == 'ballerina-platform' }}
uses: ballerina-platform/ballerina-library/.github/workflows/build-timestamp-master-template.yml@main
uses: ballerina-platform/ballerina-library/.github/workflows/build-timestamp-master-template.yml@2201.10.x
secrets: inherit
3 changes: 2 additions & 1 deletion .github/workflows/build-with-bal-test-graalvm.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ on:
pull_request:
branches:
- master
- 2201.10.x
types: [ opened, synchronize, reopened, labeled, unlabeled ]

concurrency:
Expand All @@ -30,7 +31,7 @@ jobs:
call_stdlib_workflow:
name: Run StdLib Workflow
if: ${{ github.event_name != 'schedule' || (github.event_name == 'schedule' && github.repository_owner == 'ballerina-platform') }}
uses: ballerina-platform/ballerina-library/.github/workflows/build-with-bal-test-graalvm-template.yml@main
uses: ballerina-platform/ballerina-library/.github/workflows/build-with-bal-test-graalvm-template.yml@2201.10.x
with:
lang_tag: ${{ inputs.lang_tag }}
lang_version: ${{ inputs.lang_version }}
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/central-publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ jobs:
call_workflow:
name: Run Central Publish Workflow
if: ${{ github.repository_owner == 'ballerina-platform' }}
uses: ballerina-platform/ballerina-library/.github/workflows/central-publish-template.yml@main
uses: ballerina-platform/ballerina-library/.github/workflows/central-publish-template.yml@2201.10.x
secrets: inherit
with:
environment: ${{ github.event.inputs.environment }}
2 changes: 1 addition & 1 deletion .github/workflows/publish-release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ jobs:
call_workflow:
name: Run Release Workflow
if: ${{ github.repository_owner == 'ballerina-platform' }}
uses: ballerina-platform/ballerina-library/.github/workflows/release-package-template.yml@main
uses: ballerina-platform/ballerina-library/.github/workflows/release-package-template.yml@2201.10.x
secrets: inherit
with:
package-name: xmldata
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/pull-request.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,5 @@ jobs:
call_workflow:
name: Run PR Build Workflow
if: ${{ github.repository_owner == 'ballerina-platform' }}
uses: ballerina-platform/ballerina-library/.github/workflows/pull-request-build-template.yml@main
uses: ballerina-platform/ballerina-library/.github/workflows/pull-request-build-template.yml@2201.10.x
secrets: inherit
2 changes: 1 addition & 1 deletion .github/workflows/trivy-scan.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,5 @@ jobs:
call_workflow:
name: Run Trivy Scan Workflow
if: ${{ github.repository_owner == 'ballerina-platform' }}
uses: ballerina-platform/ballerina-library/.github/workflows/trivy-scan-template.yml@main
uses: ballerina-platform/ballerina-library/.github/workflows/trivy-scan-template.yml@2201.10.x
secrets: inherit
62 changes: 62 additions & 0 deletions ballerina/tests/from_xml_test.bal
Original file line number Diff line number Diff line change
Expand Up @@ -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 `<book><name>Sherlock Holmes</name>
<details xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<author xsi:nil="true"/>
<language>English</language>
</details></book>`;
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 `<book><name>Sherlock Holmes</name>
<details xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<author xsi:nil="true"/>
<language>English</language>
</details></book>`;
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 '()'"));
}
}
10 changes: 10 additions & 0 deletions ballerina/tests/xml_from_json_test.bal
Original file line number Diff line number Diff line change
Expand Up @@ -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 `<book><name>Sherlock Holmes</name><details><author/><language>English</language></details></book>`;
test:assertEquals(result, expected, msg = "testFromJsonWithNull result incorrect");
}
27 changes: 27 additions & 0 deletions ballerina/tests/xml_to_json_test.bal
Original file line number Diff line number Diff line change
Expand Up @@ -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 `<name>Sherlock Holmes</name>
<details xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<author xsi:nil="true"/>
<language>English</language>
</details>`;
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 `<name>Sherlock Holmes</name>
<details xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<author xsi:nil="true"/>
<language>English</language>
</details>`;
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");
}
5 changes: 5 additions & 0 deletions changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
19 changes: 14 additions & 5 deletions native/src/main/java/io/ballerina/stdlib/xmldata/XmlToJson.java
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -224,7 +232,8 @@ private static void processAttributeWithAnnotation(BXmlItem xmlItem, String attr
@SuppressWarnings("unchecked")
private static BMap<BString, Object> insertDataToMap(BMap<BString, Object> childrenData, Object children,
BMap<BString, Object> 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<BString, Object> data = (BMap<BString, Object>) children;
Expand All @@ -248,9 +257,9 @@ private static BMap<BString, Object> insertDataToMap(BMap<BString, Object> 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);
Expand Down Expand Up @@ -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;
Expand Down
Loading