diff --git a/apps/metadata-utils/src/fieldHelpers.ts b/apps/metadata-utils/src/fieldHelpers.ts
index ab14bed241..0d71684404 100644
--- a/apps/metadata-utils/src/fieldHelpers.ts
+++ b/apps/metadata-utils/src/fieldHelpers.ts
@@ -32,6 +32,7 @@ export const fieldTypes = () => {
"TEXT_ARRAY",
"UUID",
"UUID_ARRAY",
+ "JSON",
];
};
@@ -56,7 +57,8 @@ export const isValueType = (column: IColumn) => {
column.columnType === "DATETIME" ||
column.columnType === "INT" ||
column.columnType === "LONG" ||
- column.columnType === "DECIMAL"
+ column.columnType === "DECIMAL" ||
+ column.columnType === "JSON"
);
};
diff --git a/apps/metadata-utils/src/types.ts b/apps/metadata-utils/src/types.ts
index 300eca7fa1..2aab5dfeb6 100644
--- a/apps/metadata-utils/src/types.ts
+++ b/apps/metadata-utils/src/types.ts
@@ -30,8 +30,7 @@ export type CellValueType =
| "DATETIME"
| "DATETIME_ARRAY"
| "PERIOD"
- | "JSONB"
- | "JSONB_ARRAY"
+ | "JSON"
| "REF"
| "REF_ARRAY"
| "REFBACK"
diff --git a/apps/molgenis-components/lib/main.js b/apps/molgenis-components/lib/main.js
index 17a41f446d..e98009dd43 100644
--- a/apps/molgenis-components/lib/main.js
+++ b/apps/molgenis-components/lib/main.js
@@ -53,6 +53,7 @@ import InputDate from "../src/components/forms/InputDate.vue";
import InputDateTime from "../src/components/forms/InputDateTime.vue";
import InputDecimal from "../src/components/forms/InputDecimal.vue";
import InputEmail from "../src/components/forms/InputEmail.vue";
+import InputJson from "../src/components/forms/InputJson.vue";
import InputFile from "../src/components/forms/InputFile.vue";
import InputGroup from "../src/components/forms/InputGroup.vue";
import InputHeading from "../src/components/forms/InputHeading.vue";
@@ -189,6 +190,7 @@ export {
InputDateTime,
InputDecimal,
InputEmail,
+ InputJson,
InputFile,
InputGroup,
InputHeading,
diff --git a/apps/molgenis-components/src/components/filters/FilterInput.vue b/apps/molgenis-components/src/components/filters/FilterInput.vue
index e22194fe4b..e454b4b732 100644
--- a/apps/molgenis-components/src/components/filters/FilterInput.vue
+++ b/apps/molgenis-components/src/components/filters/FilterInput.vue
@@ -48,6 +48,7 @@ const filterTypeMap = {
HYPERLINK_ARRAY: StringFilter,
TEXT: StringFilter,
TEXT_ARRAY: StringFilter,
+ JSON: StringFilter,
UUID: StringFilter,
UUID_ARRAY: StringFilter,
INT: IntegerFilter,
diff --git a/apps/molgenis-components/src/components/forms/FormInput.vue b/apps/molgenis-components/src/components/forms/FormInput.vue
index 9d4992dc9e..eddb232438 100644
--- a/apps/molgenis-components/src/components/forms/FormInput.vue
+++ b/apps/molgenis-components/src/components/forms/FormInput.vue
@@ -34,6 +34,7 @@ import InputText from "../forms/InputText.vue";
import BaseInput from "../forms/baseInputs/BaseInput.vue";
import InputEmail from "./InputEmail.vue";
import InputHyperlink from "./InputHyperlink.vue";
+import InputJson from "./InputJson.vue";
import InputRefList from "./InputRefList.vue";
const typeToInputMap = {
@@ -43,6 +44,7 @@ const typeToInputMap = {
HYPERLINK: InputHyperlink,
STRING: InputString,
TEXT: InputText,
+ JSON: InputJson,
INT: InputInt,
LONG: InputLong,
DECIMAL: InputDecimal,
@@ -368,6 +370,17 @@ export default {
You typed: {{ JSON.stringify(textValueArray, null, 2) }}
+
+
+
+
+ You typed: {{ jsonValue }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ You typed: {{ value }}
+ Readonly
+
+
+
+
+
diff --git a/apps/molgenis-components/src/components/forms/formUtils/formUtils.test.ts b/apps/molgenis-components/src/components/forms/formUtils/formUtils.test.ts
index 62fc4f2be3..272d73932c 100644
--- a/apps/molgenis-components/src/components/forms/formUtils/formUtils.test.ts
+++ b/apps/molgenis-components/src/components/forms/formUtils/formUtils.test.ts
@@ -9,6 +9,7 @@ import {
splitColumnIdsByHeadings,
isMissingValue,
isRequired,
+ isJsonObjectOrArray,
} from "./formUtils";
import type { ITableMetaData, IColumn } from "metadata-utils";
const { AUTO_ID, HEADING } = constants;
@@ -375,4 +376,23 @@ describe("isValidHyperLink", () => {
constants.HYPERLINK_REGEX.test("https://example.com/(test)".toLowerCase())
).toBe(true);
});
+
+ describe("isJsonObjectOrArray", () => {
+ test("only JSON object/array should return true (after parsing from JSON string)", () => {
+ expect(isJsonObjectOrArray(JSON.parse('{"key":"value"}'))).toBe(true);
+ expect(isJsonObjectOrArray(JSON.parse('["string1", "string2"]'))).toBe(
+ true
+ );
+ expect(
+ isJsonObjectOrArray(
+ JSON.parse('{"key1":{"key2":["value1", "value2"]}}')
+ )
+ ).toBe(true);
+ expect(isJsonObjectOrArray(JSON.parse('"string"'))).toBe(false);
+ expect(isJsonObjectOrArray(JSON.parse("1"))).toBe(false);
+ expect(isJsonObjectOrArray(JSON.parse("true"))).toBe(false);
+ expect(isJsonObjectOrArray(JSON.parse("false"))).toBe(false);
+ expect(isJsonObjectOrArray(JSON.parse("null"))).toBe(false);
+ });
+ });
});
diff --git a/apps/molgenis-components/src/components/forms/formUtils/formUtils.ts b/apps/molgenis-components/src/components/forms/formUtils/formUtils.ts
index 0a571226a9..897371d118 100644
--- a/apps/molgenis-components/src/components/forms/formUtils/formUtils.ts
+++ b/apps/molgenis-components/src/components/forms/formUtils/formUtils.ts
@@ -84,6 +84,15 @@ function getColumnError(
if (type === "PERIOD_ARRAY" && containsInvalidPeriod(value)) {
return "Invalid Period: should start with a P and should contain at least a Y(year), M(month) or D(day): e.g. 'P1Y3M14D'";
}
+ if (type === "JSON") {
+ try {
+ if (!isJsonObjectOrArray(JSON.parse(value))) {
+ return `Root element must be an object or array`;
+ }
+ } catch {
+ return `Please enter valid JSON`;
+ }
+ }
if (column.validation) {
return getColumnValidationError(column.validation, rowData, tableMetaData);
}
@@ -227,6 +236,13 @@ function containsInvalidPeriod(periods: any) {
return periods.find((period: any) => !isValidPeriod(period));
}
+export function isJsonObjectOrArray(parsedJson: any) {
+ if (typeof parsedJson === "object" && parsedJson !== null) {
+ return true;
+ }
+ return false;
+}
+
export function removeKeyColumns(tableMetaData: ITableMetaData, rowData: IRow) {
const keyColumnsIds = tableMetaData?.columns
?.filter((column: IColumn) => column.key === 1)
diff --git a/apps/molgenis-components/src/components/tables/DataDisplayCell.vue b/apps/molgenis-components/src/components/tables/DataDisplayCell.vue
index 2e3af4ed1e..7d4aaa52f4 100644
--- a/apps/molgenis-components/src/components/tables/DataDisplayCell.vue
+++ b/apps/molgenis-components/src/components/tables/DataDisplayCell.vue
@@ -20,6 +20,7 @@ import HyperlinkDisplay from "./cellTypes/HyperlinkDisplay.vue";
const typeMap: { [key: string]: string } = {
FILE: "FileDisplay",
TEXT: "TextDisplay",
+ JSON: "TextDisplay",
REFBACK: "ListDisplay",
REF: "ObjectDisplay",
ONTOLOGY: "ObjectDisplay",
diff --git a/apps/molgenis-components/src/components/tables/TableExplorer.vue b/apps/molgenis-components/src/components/tables/TableExplorer.vue
index 5735b237aa..987bc58535 100644
--- a/apps/molgenis-components/src/components/tables/TableExplorer.vue
+++ b/apps/molgenis-components/src/components/tables/TableExplorer.vue
@@ -835,7 +835,8 @@ function graphqlFilter(
if (conditions.length) {
if (
col.columnType.startsWith("STRING") ||
- col.columnType.startsWith("TEXT")
+ col.columnType.startsWith("TEXT") ||
+ col.columnType.startsWith("JSON")
) {
filter[col.id] = { like: conditions };
} else if (col.columnType.startsWith("BOOL")) {
diff --git a/apps/schema/src/columnTypes.js b/apps/schema/src/columnTypes.js
index e464276411..3e1e084330 100644
--- a/apps/schema/src/columnTypes.js
+++ b/apps/schema/src/columnTypes.js
@@ -16,6 +16,7 @@ export default [
"HYPERLINK_ARRAY",
"INT",
"INT_ARRAY",
+ "JSON",
"LONG",
"LONG_ARRAY",
"ONTOLOGY",
diff --git a/backend/molgenis-emx2-graphql/src/main/java/org/molgenis/emx2/graphql/GraphqlCustomTypes.java b/backend/molgenis-emx2-graphql/src/main/java/org/molgenis/emx2/graphql/GraphqlCustomTypes.java
index 562d402030..986a5468f3 100644
--- a/backend/molgenis-emx2-graphql/src/main/java/org/molgenis/emx2/graphql/GraphqlCustomTypes.java
+++ b/backend/molgenis-emx2-graphql/src/main/java/org/molgenis/emx2/graphql/GraphqlCustomTypes.java
@@ -1,5 +1,6 @@
package org.molgenis.emx2.graphql;
+import com.fasterxml.jackson.core.JsonProcessingException;
import graphql.schema.*;
import jakarta.servlet.http.Part;
import java.io.ByteArrayOutputStream;
@@ -7,13 +8,50 @@
import java.io.InputStream;
import java.util.Map;
import org.molgenis.emx2.BinaryFileWrapper;
+import org.molgenis.emx2.utils.MolgenisObjectMapper;
public class GraphqlCustomTypes {
+ private static final MolgenisObjectMapper objectMapper = MolgenisObjectMapper.INTERNAL;
private GraphqlCustomTypes() {
// hide constructor
}
+ public static final GraphQLScalarType GraphQLJsonAsString =
+ GraphQLScalarType.newScalar()
+ .name("JsonString")
+ .description("A JSON represented as string")
+ .coercing(
+ new Coercing() {
+
+ @Override
+ public String serialize(Object dataFetcherResult) {
+ // Convert Java object to JSON string
+ try {
+ return objectMapper.getWriter().writeValueAsString(dataFetcherResult);
+ } catch (JsonProcessingException e) {
+ throw new CoercingSerializeException(
+ "Unable to serialize to JSON string: " + e.getMessage());
+ }
+ }
+
+ @Override
+ public String parseValue(Object input) {
+ // Pass-through parsing (only used for input values)
+ return input.toString();
+ }
+
+ @Override
+ public String parseLiteral(Object input) {
+ // Pass-through literal parsing
+ if (input instanceof graphql.language.StringValue) {
+ return ((graphql.language.StringValue) input).getValue();
+ }
+ throw new CoercingParseLiteralException("Value is not a valid JSON string");
+ }
+ })
+ .build();
+
// thanks to https://stackoverflow.com/questions/57372259/how-to-upload-files-with-graphql-java
public static final GraphQLScalarType GraphQLFileUpload =
GraphQLScalarType.newScalar()
diff --git a/backend/molgenis-emx2-graphql/src/main/java/org/molgenis/emx2/graphql/GraphqlTableFieldFactory.java b/backend/molgenis-emx2-graphql/src/main/java/org/molgenis/emx2/graphql/GraphqlTableFieldFactory.java
index 21e4278cf9..dc4c42ef46 100644
--- a/backend/molgenis-emx2-graphql/src/main/java/org/molgenis/emx2/graphql/GraphqlTableFieldFactory.java
+++ b/backend/molgenis-emx2-graphql/src/main/java/org/molgenis/emx2/graphql/GraphqlTableFieldFactory.java
@@ -8,6 +8,7 @@
import static org.molgenis.emx2.graphql.GraphqlApiMutationResult.Status.SUCCESS;
import static org.molgenis.emx2.graphql.GraphqlApiMutationResult.typeForMutationResult;
import static org.molgenis.emx2.graphql.GraphqlConstants.*;
+import static org.molgenis.emx2.graphql.GraphqlCustomTypes.GraphQLJsonAsString;
import static org.molgenis.emx2.sql.SqlQuery.*;
import graphql.Scalars;
@@ -187,6 +188,10 @@ private void createTableField(Column col, GraphQLObjectType.Builder tableBuilder
tableBuilder.field(
GraphQLFieldDefinition.newFieldDefinition().name(id).type(Scalars.GraphQLString));
break;
+ case JSON:
+ tableBuilder.field(
+ GraphQLFieldDefinition.newFieldDefinition().name(id).type(GraphQLJsonAsString));
+ break;
case STRING_ARRAY:
case EMAIL_ARRAY:
case HYPERLINK_ARRAY:
@@ -196,7 +201,6 @@ private void createTableField(Column col, GraphQLObjectType.Builder tableBuilder
case DATETIME_ARRAY:
case PERIOD_ARRAY:
case UUID_ARRAY:
- case JSONB_ARRAY:
tableBuilder.field(
GraphQLFieldDefinition.newFieldDefinition()
.name(id)
@@ -507,6 +511,8 @@ private GraphQLScalarType graphQLTypeOf(Column col) {
return GraphQLLong;
case DECIMAL, DECIMAL_ARRAY:
return Scalars.GraphQLFloat;
+ case JSON:
+ return GraphQLJsonAsString;
case DATE,
DATETIME,
PERIOD,
@@ -908,6 +914,7 @@ private GraphQLInputType getGraphQLInputType(ColumnType columnType) {
case INT_ARRAY -> GraphQLList.list(Scalars.GraphQLInt);
case LONG_ARRAY -> GraphQLList.list(GraphQLLong);
case DECIMAL_ARRAY -> GraphQLList.list(Scalars.GraphQLFloat);
+ case JSON -> GraphqlCustomTypes.GraphQLJsonAsString;
case STRING_ARRAY,
TEXT_ARRAY,
DATE_ARRAY,
diff --git a/backend/molgenis-emx2-graphql/src/test/java/org/molgenis/emx2/graphql/TestGraphqlSchemaFields.java b/backend/molgenis-emx2-graphql/src/test/java/org/molgenis/emx2/graphql/TestGraphqlSchemaFields.java
index cb35c22fd7..ab93425dd2 100644
--- a/backend/molgenis-emx2-graphql/src/test/java/org/molgenis/emx2/graphql/TestGraphqlSchemaFields.java
+++ b/backend/molgenis-emx2-graphql/src/test/java/org/molgenis/emx2/graphql/TestGraphqlSchemaFields.java
@@ -739,6 +739,48 @@ public void testTableType() throws IOException {
assertEquals("ONTOLOGIES", result.at("/_schema/tables/3/tableType").asText());
}
+ @Test
+ public void testJsonType() throws IOException {
+ try {
+ Schema myschema = database.dropCreateSchema("testJsonType");
+ myschema.create(
+ table("TestJson", column("name").setPkey(), column("json").setType(ColumnType.JSON)));
+
+ grapql = new GraphqlApiFactory().createGraphqlForSchema(myschema, taskService);
+
+ Table table = myschema.getTable("TestJson");
+ String value = "{\"name\":\"bofke\"}";
+ table.insert(row("name", "test", "json", value));
+
+ assertEquals(value, execute("{TestJson{json}}").at("/TestJson/0/json").asText());
+
+ String value2 = "{\"name\":\"bofke2\"}";
+ Map data = new LinkedHashMap();
+ data.put("name", "test");
+ data.put("json", value2);
+ grapql.execute(
+ new ExecutionInput.Builder()
+ .query("mutation update($value:[TestJsonInput]){update(TestJson:$value){message}}")
+ .variables(Map.of("value", data))
+ .build());
+
+ assertEquals(value2, execute("{TestJson{json}}").at("/TestJson/0/json").asText());
+ assertEquals(
+ value2,
+ execute(
+ "{TestJson(filter:{json:{equals:\"{\\\"name\\\": \\\"bofke2\\\"}\"}}){json}}") // notice the extra space!
+ .at("/TestJson/0/json")
+ .asText());
+ assertEquals(
+ value2,
+ execute("{TestJson(filter:{json:{like:\"bofke2\"}}){json}}") // more useful
+ .at("/TestJson/0/json")
+ .asText());
+ } finally {
+ grapql = new GraphqlApiFactory().createGraphqlForSchema(schema, taskService);
+ }
+ }
+
@Test
public void testFileType() throws IOException {
try {
diff --git a/backend/molgenis-emx2-io/src/main/java/org/molgenis/emx2/io/emx1/Emx1Attribute.java b/backend/molgenis-emx2-io/src/main/java/org/molgenis/emx2/io/emx1/Emx1Attribute.java
index d53fe98283..e31765b710 100644
--- a/backend/molgenis-emx2-io/src/main/java/org/molgenis/emx2/io/emx1/Emx1Attribute.java
+++ b/backend/molgenis-emx2-io/src/main/java/org/molgenis/emx2/io/emx1/Emx1Attribute.java
@@ -257,7 +257,7 @@ private static String getEmx1Type(Column c) {
return "file";
case STRING, UUID:
return "varchar";
- case TEXT, JSONB:
+ case TEXT, JSON:
return "text";
case INT:
return "int";
@@ -280,7 +280,6 @@ private static String getEmx1Type(Column c) {
INT_ARRAY,
DATE_ARRAY,
DATETIME_ARRAY,
- JSONB_ARRAY,
DECIMAL_ARRAY:
return "array types unsupported in emx1: " + c.getColumnType();
default:
diff --git a/backend/molgenis-emx2-rdf/src/main/java/org/molgenis/emx2/rdf/ColumnTypeRdfMapper.java b/backend/molgenis-emx2-rdf/src/main/java/org/molgenis/emx2/rdf/ColumnTypeRdfMapper.java
index 353ed53ebc..2d8f7a3a1f 100644
--- a/backend/molgenis-emx2-rdf/src/main/java/org/molgenis/emx2/rdf/ColumnTypeRdfMapper.java
+++ b/backend/molgenis-emx2-rdf/src/main/java/org/molgenis/emx2/rdf/ColumnTypeRdfMapper.java
@@ -47,6 +47,7 @@ public class ColumnTypeRdfMapper {
entry(ColumnType.STRING_ARRAY, RdfColumnType.STRING),
entry(ColumnType.TEXT, RdfColumnType.STRING),
entry(ColumnType.TEXT_ARRAY, RdfColumnType.STRING),
+ entry(ColumnType.JSON, RdfColumnType.STRING),
// NUMERIC
entry(ColumnType.INT, RdfColumnType.INT),
@@ -62,10 +63,6 @@ public class ColumnTypeRdfMapper {
entry(ColumnType.PERIOD, RdfColumnType.DURATION),
entry(ColumnType.PERIOD_ARRAY, RdfColumnType.DURATION),
- // COMPOSITE
- entry(ColumnType.JSONB, RdfColumnType.STRING),
- entry(ColumnType.JSONB_ARRAY, RdfColumnType.SKIP), // Unsupported.
-
// RELATIONSHIP
entry(ColumnType.REF, RdfColumnType.REFERENCE),
entry(ColumnType.REF_ARRAY, RdfColumnType.REFERENCE),
diff --git a/backend/molgenis-emx2-rdf/src/test/java/org/molgenis/emx2/rdf/ColumnTypeRdfMapperTest.java b/backend/molgenis-emx2-rdf/src/test/java/org/molgenis/emx2/rdf/ColumnTypeRdfMapperTest.java
index bc71a1d7d7..d773b9ed2a 100644
--- a/backend/molgenis-emx2-rdf/src/test/java/org/molgenis/emx2/rdf/ColumnTypeRdfMapperTest.java
+++ b/backend/molgenis-emx2-rdf/src/test/java/org/molgenis/emx2/rdf/ColumnTypeRdfMapperTest.java
@@ -121,6 +121,9 @@ public static void setup() {
"lonelyText",
ColumnType.TEXT_ARRAY.name(),
"text1,text2",
+ ColumnType.JSON.name(),
+ "{\"a\":1,\"b\":2}",
+ // NUMERIC
ColumnType.INT.name(),
"0",
ColumnType.INT_ARRAY.name(),
@@ -145,11 +148,6 @@ public static void setup() {
"P1D",
ColumnType.PERIOD_ARRAY.name(),
"P1M,P1Y",
- // COMPOSITE
- ColumnType.JSONB.name(),
- "{\"a\":1,\"b\":2}",
- ColumnType.JSONB_ARRAY.name(), // TODO: Remove if deprecated.
- "{\"c\":3},{\"d\":4,\"e\":5}",
// RELATIONSHIP
ColumnType.REF.name(),
"1",
@@ -231,6 +229,7 @@ void validateValueTypes() {
// STRING
() -> Assertions.assertTrue(retrieveFirstValue(ColumnType.STRING.name()).isLiteral()),
() -> Assertions.assertTrue(retrieveFirstValue(ColumnType.TEXT.name()).isLiteral()),
+ () -> Assertions.assertTrue(retrieveFirstValue(ColumnType.JSON.name()).isLiteral()),
// NUMERIC
() -> Assertions.assertTrue(retrieveFirstValue(ColumnType.INT.name()).isLiteral()),
@@ -240,9 +239,6 @@ void validateValueTypes() {
() -> Assertions.assertTrue(retrieveFirstValue(ColumnType.DATETIME.name()).isLiteral()),
() -> Assertions.assertTrue(retrieveFirstValue(ColumnType.PERIOD.name()).isLiteral()),
- // COMPOSITE
- () -> Assertions.assertTrue(retrieveFirstValue(ColumnType.JSONB.name()).isLiteral()),
-
// RELATIONSHIP
() -> Assertions.assertTrue(retrieveFirstValue(ColumnType.REF.name()).isIRI()),
() -> Assertions.assertTrue(retrieveFirstValue(ColumnType.REFBACK.name()).isIRI()),
@@ -290,6 +286,7 @@ void validateValuesRetrieval() {
// Not sure how to retrieve more directly as changes everytime
+ firstRow.getString(ColumnType.FILE.name()))),
retrieveValues(ColumnType.FILE.name())),
+
// STRING
() ->
Assertions.assertEquals(
@@ -311,6 +308,11 @@ void validateValuesRetrieval() {
Values.literal("text1", CoreDatatype.XSD.STRING),
Values.literal("text2", CoreDatatype.XSD.STRING)),
retrieveValues(ColumnType.TEXT_ARRAY.name())),
+ () ->
+ Assertions.assertEquals(
+ Set.of(Values.literal("{\"a\":1,\"b\":2}", CoreDatatype.XSD.STRING)),
+ retrieveValues(ColumnType.JSON.name())),
+
// NUMERIC
() ->
Assertions.assertEquals(
@@ -364,13 +366,6 @@ void validateValuesRetrieval() {
Values.literal("P1Y", CoreDatatype.XSD.DURATION)),
retrieveValues(ColumnType.PERIOD_ARRAY.name())),
- // COMPOSITE
- () ->
- Assertions.assertEquals(
- Set.of(Values.literal("{\"a\":1,\"b\":2}", CoreDatatype.XSD.STRING)),
- retrieveValues(ColumnType.JSONB.name())),
- // TODO: Remove if deprecated.
- () -> Assertions.assertEquals(Set.of(), retrieveValues(ColumnType.JSONB_ARRAY.name())),
// RELATIONSHIP
() ->
Assertions.assertEquals(
@@ -386,6 +381,7 @@ void validateValuesRetrieval() {
Assertions.assertEquals(
Set.of(Values.iri(rdfApiUrlPrefix + REFBACK_TABLE + "?id=1")),
retrieveValues(ColumnType.REFBACK.name())),
+
// LAYOUT and other constants -> should return empty sets as they should be excluded
() -> Assertions.assertEquals(Set.of(), retrieveValues(ColumnType.HEADING.name())),
// format flavors that extend a baseType
diff --git a/backend/molgenis-emx2-sql/src/main/java/org/molgenis/emx2/sql/SqlQuery.java b/backend/molgenis-emx2-sql/src/main/java/org/molgenis/emx2/sql/SqlQuery.java
index 30bdefa87e..845522b3ac 100644
--- a/backend/molgenis-emx2-sql/src/main/java/org/molgenis/emx2/sql/SqlQuery.java
+++ b/backend/molgenis-emx2-sql/src/main/java/org/molgenis/emx2/sql/SqlQuery.java
@@ -1,6 +1,7 @@
package org.molgenis.emx2.sql;
import static org.jooq.impl.DSL.*;
+import static org.jooq.impl.SQLDataType.VARCHAR;
import static org.molgenis.emx2.Constants.*;
import static org.molgenis.emx2.Operator.*;
import static org.molgenis.emx2.Privileges.*;
@@ -1139,10 +1140,9 @@ private Condition whereCondition(
Object[] values) {
Name name = name(alias(tableAlias), columnName);
return switch (type) {
- case TEXT, STRING, FILE -> whereConditionText(name, operator, toStringArray(values));
+ case TEXT, STRING, FILE, JSON -> whereConditionText(name, operator, toStringArray(values));
case BOOL -> whereConditionEquals(name, operator, toBoolArray(values));
case UUID -> whereConditionEquals(name, operator, toUuidArray(values));
- case JSONB -> whereConditionEquals(name, operator, toJsonbArray(values));
case INT -> whereConditionOrdinal(name, operator, toIntArray(values));
case LONG -> whereConditionOrdinal(name, operator, toLongArray(values));
case DECIMAL -> whereConditionOrdinal(name, operator, toDecimalArray(values));
@@ -1159,7 +1159,6 @@ private Condition whereCondition(
case DATE_ARRAY -> whereConditionArrayEquals(name, operator, toDateArray(values));
case DATETIME_ARRAY -> whereConditionArrayEquals(name, operator, toDateTimeArray(values));
case PERIOD_ARRAY -> whereConditionArrayEquals(name, operator, toYearToSecondArray(values));
- case JSONB_ARRAY -> whereConditionArrayEquals(name, operator, toJsonbArray(values));
case REF -> whereConditionRefEquals(name, operator, values);
default ->
throw new SqlQueryException(
@@ -1292,7 +1291,7 @@ private static Condition whereConditionText(
for (String value : values) {
switch (operator) {
case EQUALS:
- conditions.add(field(columnName).eq(value));
+ conditions.add(field(columnName).cast(VARCHAR).eq(value)); // cast is for the json
break;
case NOT_EQUALS:
not = true;
diff --git a/backend/molgenis-emx2-sql/src/main/java/org/molgenis/emx2/sql/SqlTypeUtils.java b/backend/molgenis-emx2-sql/src/main/java/org/molgenis/emx2/sql/SqlTypeUtils.java
index d6115604f4..9b5551d11b 100644
--- a/backend/molgenis-emx2-sql/src/main/java/org/molgenis/emx2/sql/SqlTypeUtils.java
+++ b/backend/molgenis-emx2-sql/src/main/java/org/molgenis/emx2/sql/SqlTypeUtils.java
@@ -179,8 +179,7 @@ public static Object getTypedValue(Column c, Row row) {
case DATETIME_ARRAY -> row.getDateTimeArray(name);
case PERIOD -> row.getPeriod(name);
case PERIOD_ARRAY -> row.getPeriodArray(name);
- case JSONB -> row.getJsonb(name);
- case JSONB_ARRAY -> row.getJsonbArray(name);
+ case JSON -> row.getJsonb(name);
default ->
throw new UnsupportedOperationException(
"Unsupported columnType found:" + c.getColumnType());
@@ -209,7 +208,7 @@ static String getPsqlType(ColumnType type) {
case DATE_ARRAY -> "date[]";
case DATETIME -> "timestamp without time zone";
case DATETIME_ARRAY -> "timestamp without time zone[]";
- case JSONB -> "jsonb";
+ case JSON -> "jsonb";
default ->
throw new MolgenisException(
"Unknown type: Internal error: data cannot be mapped to psqlType " + type);
diff --git a/backend/molgenis-emx2-sql/src/test/java/org/molgenis/emx2/sql/TestCreateArrayDataTypes.java b/backend/molgenis-emx2-sql/src/test/java/org/molgenis/emx2/sql/TestCreateArrayDataTypes.java
index 43805b55e3..64eddf3ec2 100644
--- a/backend/molgenis-emx2-sql/src/test/java/org/molgenis/emx2/sql/TestCreateArrayDataTypes.java
+++ b/backend/molgenis-emx2-sql/src/test/java/org/molgenis/emx2/sql/TestCreateArrayDataTypes.java
@@ -80,13 +80,6 @@ public void testTextArray() {
});
}
- @Test
- public void testJSON() {
- executeTest(
- JSONB_ARRAY,
- new String[] {"{\"key\":\"value1\"}", "{\"key\":\"value2\"}", "{\"key\":\"value3\"}"});
- }
-
// @Test
// public void testBool() {
// executeTest(BOOL_ARRAY, new Boolean[] {null, true, false});
diff --git a/backend/molgenis-emx2-sql/src/test/java/org/molgenis/emx2/sql/TestCreateBasicDataColumnTypeColumns.java b/backend/molgenis-emx2-sql/src/test/java/org/molgenis/emx2/sql/TestCreateBasicDataColumnTypeColumns.java
index ae6b115a79..6053b37f24 100644
--- a/backend/molgenis-emx2-sql/src/test/java/org/molgenis/emx2/sql/TestCreateBasicDataColumnTypeColumns.java
+++ b/backend/molgenis-emx2-sql/src/test/java/org/molgenis/emx2/sql/TestCreateBasicDataColumnTypeColumns.java
@@ -165,8 +165,8 @@ public void testText() {
@Test
public void testJSON() {
executeTest(
- JSONB,
- new String[] {"{\"key\":\"value1\"}", "{\"key\":\"value2\"}", "{\"key\":\"value3\"}"});
+ JSON,
+ new String[] {"{\"key\": \"value1\"}", "{\"key\": \"value2\"}", "{\"key\": \"value3\"}"});
}
private void executeTest(ColumnType columnType, Serializable[] values) {
diff --git a/backend/molgenis-emx2-sql/src/test/java/org/molgenis/emx2/sql/TestCreateForeignKeys.java b/backend/molgenis-emx2-sql/src/test/java/org/molgenis/emx2/sql/TestCreateForeignKeys.java
index 30371d3b9d..9a728fb804 100644
--- a/backend/molgenis-emx2-sql/src/test/java/org/molgenis/emx2/sql/TestCreateForeignKeys.java
+++ b/backend/molgenis-emx2-sql/src/test/java/org/molgenis/emx2/sql/TestCreateForeignKeys.java
@@ -56,7 +56,7 @@ public void testUUID() {
@Test
public void testJSON() {
- executeTest(JSONB, "{\"key\": \"value1\"}", "{\"key\": \"value2\"}");
+ executeTest(JSON, "{\"key\": \"value1\"}", "{\"key\": \"value2\"}");
}
private void executeTest(ColumnType columnType, Object insertValue, Object updateValue) {
diff --git a/backend/molgenis-emx2-sql/src/test/java/org/molgenis/emx2/sql/TestCreateForeignKeysArrays.java b/backend/molgenis-emx2-sql/src/test/java/org/molgenis/emx2/sql/TestCreateForeignKeysArrays.java
index 273584592b..610f0484bd 100644
--- a/backend/molgenis-emx2-sql/src/test/java/org/molgenis/emx2/sql/TestCreateForeignKeysArrays.java
+++ b/backend/molgenis-emx2-sql/src/test/java/org/molgenis/emx2/sql/TestCreateForeignKeysArrays.java
@@ -67,13 +67,6 @@ public void testTextRef() {
});
}
- @Test
- public void testJSONRef() {
- executeTest(
- ColumnType.JSONB,
- new String[] {"{\"key\": \"value1\"}", "{\"key\": \"value2\"}", "{\"key\": \"value3\"}"});
- }
-
private void executeTest(ColumnType columnType, Object[] testValues) {
Schema schema = db.dropCreateSchema("TestRefArray" + columnType.toString().toUpperCase());
diff --git a/backend/molgenis-emx2/src/main/java/org/molgenis/emx2/ColumnType.java b/backend/molgenis-emx2/src/main/java/org/molgenis/emx2/ColumnType.java
index 6b6763626e..0c869610e1 100644
--- a/backend/molgenis-emx2/src/main/java/org/molgenis/emx2/ColumnType.java
+++ b/backend/molgenis-emx2/src/main/java/org/molgenis/emx2/ColumnType.java
@@ -20,6 +20,7 @@ public enum ColumnType {
STRING_ARRAY(String[].class, STRING_OPERATORS),
TEXT(String.class, STRING_OPERATORS),
TEXT_ARRAY(String[].class, STRING_OPERATORS),
+ JSON(org.jooq.JSONB.class, STRING_OPERATORS),
// NUMERIC
INT(Integer.class, ORDINAL_OPERATORS),
@@ -35,10 +36,6 @@ public enum ColumnType {
PERIOD(Period.class, ORDINAL_OPERATORS),
PERIOD_ARRAY(Period[].class, ORDINAL_OPERATORS),
- // COMPOSITE
- JSONB(org.jooq.JSONB.class),
- JSONB_ARRAY(org.jooq.JSONB[].class),
-
// RELATIONSHIP
REF(Object.class),
REF_ARRAY(Object[].class),
diff --git a/backend/molgenis-emx2/src/main/java/org/molgenis/emx2/Row.java b/backend/molgenis-emx2/src/main/java/org/molgenis/emx2/Row.java
index 196213b915..35067117be 100644
--- a/backend/molgenis-emx2/src/main/java/org/molgenis/emx2/Row.java
+++ b/backend/molgenis-emx2/src/main/java/org/molgenis/emx2/Row.java
@@ -171,10 +171,6 @@ public JSONB getJsonb(String name) {
return TypeUtils.toJsonb(values.get(name));
}
- public JSONB[] getJsonbArray(String name) {
- return TypeUtils.toJsonbArray(values.get(name));
- }
-
public Row setString(String name, String value) {
this.values.put(name, value);
return this;
@@ -344,8 +340,6 @@ public T get(String name, Class type) {
return (T) getStringArray(name);
case "JSONB":
return (T) getJsonb(name);
- case "JSONB[]":
- return (T) getJsonbArray(name);
case "Integer":
return (T) getInteger(name);
case "Integer[]":
diff --git a/backend/molgenis-emx2/src/main/java/org/molgenis/emx2/utils/MolgenisObjectMapper.java b/backend/molgenis-emx2/src/main/java/org/molgenis/emx2/utils/MolgenisObjectMapper.java
new file mode 100644
index 0000000000..358fd162cd
--- /dev/null
+++ b/backend/molgenis-emx2/src/main/java/org/molgenis/emx2/utils/MolgenisObjectMapper.java
@@ -0,0 +1,70 @@
+package org.molgenis.emx2.utils;
+
+import com.fasterxml.jackson.core.JsonParser;
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.core.StreamReadFeature;
+import com.fasterxml.jackson.databind.DeserializationFeature;
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.ObjectReader;
+import com.fasterxml.jackson.databind.ObjectWriter;
+import org.molgenis.emx2.MolgenisException;
+
+public enum MolgenisObjectMapper {
+ INTERNAL {
+ @Override
+ void configureSpecific(ObjectMapper objectMapper) {
+ // Enable source inclusion in error location
+ objectMapper.configure(StreamReadFeature.INCLUDE_SOURCE_IN_LOCATION.mappedFeature(), true);
+ }
+ },
+ PUBLIC;
+
+ // Immutable singletons
+ private ObjectReader reader;
+ private ObjectWriter writer;
+
+ public ObjectReader getReader() {
+ return reader;
+ }
+
+ public ObjectWriter getWriter() {
+ return writer;
+ }
+
+ MolgenisObjectMapper() {
+ ObjectMapper objectMapper = new ObjectMapper();
+ configureGeneric(objectMapper);
+ configureSpecific(objectMapper);
+
+ this.reader = objectMapper.reader();
+ this.writer = objectMapper.writer();
+ }
+
+ private void configureGeneric(ObjectMapper objectMapper) {
+ // No trailing data allowed
+ objectMapper.configure(DeserializationFeature.FAIL_ON_TRAILING_TOKENS, true);
+ // Duplicate check reduces performance
+ objectMapper.configure(JsonParser.Feature.STRICT_DUPLICATE_DETECTION, true);
+ }
+
+ /** Override for specific configs. */
+ void configureSpecific(ObjectMapper objectMapper) {}
+
+ /**
+ * @see #validate(JsonNode)
+ */
+ public String validate(String json) throws JsonProcessingException {
+ return validate(reader.readTree(json)).toString();
+ }
+
+ /** Validate JSON if it adheres to Molgenis EMX2 specific requirements. */
+ public JsonNode validate(JsonNode rootNode) {
+ if (!(rootNode.isObject() || rootNode.isArray())) {
+ throw new MolgenisException(
+ "Only an object or array is allowed as root element. Found type is: "
+ + rootNode.getNodeType().name());
+ }
+ return rootNode;
+ }
+}
diff --git a/backend/molgenis-emx2/src/main/java/org/molgenis/emx2/utils/TypeUtils.java b/backend/molgenis-emx2/src/main/java/org/molgenis/emx2/utils/TypeUtils.java
index 955f563cf8..2bbe1354be 100644
--- a/backend/molgenis-emx2/src/main/java/org/molgenis/emx2/utils/TypeUtils.java
+++ b/backend/molgenis-emx2/src/main/java/org/molgenis/emx2/utils/TypeUtils.java
@@ -1,6 +1,6 @@
package org.molgenis.emx2.utils;
-import java.io.Serializable;
+import com.fasterxml.jackson.databind.JsonNode;
import java.lang.reflect.Array;
import java.math.BigDecimal;
import java.sql.Timestamp;
@@ -19,6 +19,8 @@
import org.molgenis.emx2.*;
public class TypeUtils {
+ private static final MolgenisObjectMapper objectMapper = MolgenisObjectMapper.INTERNAL;
+
private static final String LOOSE_PARSER_FORMAT =
"[yyyy-MM-dd]['T'[HHmmss][HHmm][HH:mm:ss][HH:mm][.SSSSSSSSS][.SSSSSSSS][.SSSSSSS][.SSSSSS][.SSSSS][.SSSS][.SSS][.SS][.S]][OOOO][O][z][XXXXX][XXXX]['['VV']']";
@@ -249,37 +251,27 @@ public static LocalDateTime[] toDateTimeArray(Object v) {
public static JSONB toJsonb(Object v) {
if (v == null) return null;
- if (v instanceof String) {
- String value = toString(v);
- if (value != null) {
- return org.jooq.JSONB.valueOf(value);
- } else {
- return null;
- }
+ if (v instanceof JSONB) { // Ensures JSONB is validated
+ v = v.toString();
}
- return (JSONB) v;
- }
-
- public static JSONB[] toJsonbArray(Object v) {
- // non standard so not using the generic function
- if (v == null) return null; // NOSONAR
if (v instanceof String) {
String value = toString(v);
if (value != null) {
- v = List.of(JSONB.valueOf(value));
+ try {
+ v = objectMapper.getReader().readTree(value);
+ } catch (Exception e) {
+ throw new MolgenisException("Invalid json", e);
+ }
} else {
return null;
}
}
- if (v instanceof String[]) {
- v = toStringArray(v);
+ if (v instanceof JsonNode) {
+ return org.jooq.JSONB.valueOf(objectMapper.validate((JsonNode) v).toString());
}
- if (v instanceof Serializable[]) v = List.of((Serializable[]) v);
- if (v instanceof Object[]) v = List.of((Object[]) v);
- if (v instanceof List) {
- return ((List