Skip to content

Commit

Permalink
Merge pull request #51 from ballerina-platform/fix-7326
Browse files Browse the repository at this point in the history
Fix `toJson` for cyclic values
  • Loading branch information
SasinduDilshara authored Nov 12, 2024
2 parents 2e473c5 + 8647dce commit 4156d57
Show file tree
Hide file tree
Showing 5 changed files with 95 additions and 13 deletions.
14 changes: 2 additions & 12 deletions ballerina/json_api.bal
Original file line number Diff line number Diff line change
Expand Up @@ -55,18 +55,8 @@ public isolated function parseStream(stream<byte[], error?> s, Options options =
#
# + v - Source anydata value
# + return - representation of `v` as value of type json
public isolated function toJson(anydata v) returns json {
if v is anydata[] {
return from anydata elem in v
select toJson(elem);
} else if v is map<anydata> {
return map from var [key, feild] in v.entries()
select [getNameAnnotation(v, key), toJson(feild)];
}
return v.toJson();
}

isolated function getNameAnnotation(map<anydata> data, string key) returns string = @java:Method {'class: "io.ballerina.lib.data.jsondata.json.Native"} external;
public isolated function toJson(anydata v) returns json =
@java:Method {'class: "io.ballerina.lib.data.jsondata.json.Native"} external;

# Prettifies a `json` value to print it.
#
Expand Down
46 changes: 46 additions & 0 deletions ballerina/tests/to_json_test.bal
Original file line number Diff line number Diff line change
Expand Up @@ -173,3 +173,49 @@ function testToJsonWithNameANnotation() {
test:assertTrue(j2 is json);
test:assertEquals(j2, out2);
}

type TestRecord4 record {|
@Name {
value: "a-o"
}
string a;
@Name {
value: "b-o"
}
string b;
int c;
TestRecord4[] d;
|};

@test:Config
function testToJsonWithCyclicValues() {
json[] v1 = [];
v1.push(v1);
json|error r1 = trap toJsonWithCyclicValues(v1);
test:assertTrue(r1 is error);
error r1Err = <error> r1;
test:assertEquals("the value has a cyclic reference", r1Err.message());

map<json> v2 = {};
v2["val"] = v2;
json|error r2 = trap toJsonWithCyclicValues(v2);
test:assertTrue(r2 is error);
error r2Err = <error> r2;
test:assertEquals("the value has a cyclic reference", r2Err.message());

TestRecord4 v3 = {
a: "a-v",
b: "b-v",
c: 1,
d: []
};
v3.d.push(v3);
json|error r3 = trap toJsonWithCyclicValues(v3);
test:assertTrue(r3 is error);
error r3Err = <error> r3;
test:assertEquals("the value has a cyclic reference", r3Err.message());
}

function toJsonWithCyclicValues(anydata val) returns json {
return toJson(val);
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,15 @@
import io.ballerina.lib.data.jsondata.io.DataReaderTask;
import io.ballerina.lib.data.jsondata.io.DataReaderThreadPool;
import io.ballerina.lib.data.jsondata.utils.Constants;
import io.ballerina.lib.data.jsondata.utils.DiagnosticErrorCode;
import io.ballerina.lib.data.jsondata.utils.DiagnosticLog;
import io.ballerina.runtime.api.Environment;
import io.ballerina.runtime.api.Future;
import io.ballerina.runtime.api.PredefinedTypes;
import io.ballerina.runtime.api.creators.TypeCreator;
import io.ballerina.runtime.api.creators.ValueCreator;
import io.ballerina.runtime.api.types.RecordType;
import io.ballerina.runtime.api.utils.JsonUtils;
import io.ballerina.runtime.api.utils.StringUtils;
import io.ballerina.runtime.api.values.BArray;
import io.ballerina.runtime.api.values.BError;
Expand All @@ -36,7 +42,9 @@
import java.io.ByteArrayInputStream;
import java.io.InputStreamReader;
import java.io.StringReader;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;

import static io.ballerina.lib.data.jsondata.json.JsonCreator.getModifiedName;
import static io.ballerina.lib.data.jsondata.utils.DataUtils.unescapeIdentifier;
Expand Down Expand Up @@ -82,6 +90,40 @@ public static Object parseStream(Environment env, BStream json, BMap<BString, Ob
return null;
}

public static Object toJson(Object value) {
return toJson(value, new HashSet<>());
}

public static Object toJson(Object value, Set<Object> visitedValues) {
if (!visitedValues.add(value)) {
throw DiagnosticLog.error(DiagnosticErrorCode.CYCLIC_REFERENCE);
}

if (value instanceof BArray listValue) {
int length = (int) listValue.getLength();
Object[] convertedValues = new Object[length];
for (int i = 0; i < length; i++) {
convertedValues[i] = toJson(listValue.get(i), visitedValues);
}
return ValueCreator.createArrayValue(convertedValues, PredefinedTypes.TYPE_JSON_ARRAY);
}

if (value instanceof BMap) {
BMap<BString, Object> mapValue = (BMap<BString, Object>) value;
BMap<BString, Object> jsonObject =
ValueCreator.createMapValue(TypeCreator.createMapType(PredefinedTypes.TYPE_JSON));

for (BString entryKey : mapValue.getKeys()) {
Object entryValue = mapValue.get(entryKey);
jsonObject.put(getNameAnnotation(mapValue, entryKey), toJson(entryValue, visitedValues));
}

return jsonObject;
}

return JsonUtils.convertToJson(value);
}

public static BString getNameAnnotation(BMap<BString, Object> value, BString key) {
if (!(value.getType() instanceof RecordType recordType)) {
return key;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,8 @@ public enum DiagnosticErrorCode {
INVALID_TYPE_FOR_FIELD("JSON_ERROR_009", "invalid.type.for.field"),
DUPLICATE_FIELD("JSON_ERROR_010", "duplicate.field"),
CANNOT_CONVERT_TO_EXPECTED_TYPE("JSON_ERROR_011", "cannot.convert.to.expected.type"),
UNDEFINED_FIELD("JSON_ERROR_012", "undefined.field");
UNDEFINED_FIELD("JSON_ERROR_012", "undefined.field"),
CYCLIC_REFERENCE("JSON_ERROR_013", "cyclic.reference");

String diagnosticId;
String messageKey;
Expand Down
3 changes: 3 additions & 0 deletions native/src/main/resources/json_error.properties
Original file line number Diff line number Diff line change
Expand Up @@ -55,3 +55,6 @@ error.cannot.convert.to.expected.type=\

error.undefined.field=\
undefined field ''{0}''

error.cyclic.reference=\
the value has a cyclic reference

0 comments on commit 4156d57

Please sign in to comment.