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

Fix toJson for cyclic values #51

Merged
merged 1 commit into from
Nov 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
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();
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Assumed this would be int for values created at runtime.

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
Loading