diff --git a/apps/opik-backend/src/main/java/com/comet/opik/utils/ValidationUtils.java b/apps/opik-backend/src/main/java/com/comet/opik/utils/ValidationUtils.java
index ffe1dde5d9..b8d140e70b 100644
--- a/apps/opik-backend/src/main/java/com/comet/opik/utils/ValidationUtils.java
+++ b/apps/opik-backend/src/main/java/com/comet/opik/utils/ValidationUtils.java
@@ -7,7 +7,25 @@
public class ValidationUtils {
- public static final String NULL_OR_NOT_BLANK = "^(?!\\s*$).+";
+ /**
+ * Regular expression to validate if a string is null or not blank.
+ *
+ *
It matches any string that is not null and contains at least one non-whitespace character.
+ * For example:
+ *
+ * - "" -> false
+ * - " " -> false
+ * - "\n" -> false
+ * - null -> true
+ * - "a" -> true
+ * - " a " -> true
+ * - "\n a \n" -> true
+ *
+ *
+ * @see Visual Explainer
+ * @see Ai Explainer
+ */
+ public static final String NULL_OR_NOT_BLANK = "(?s)^\\s*(\\S.*\\S|\\S)\\s*$";
/**
* Canonical String representation to ensure precision over float or double.
diff --git a/apps/opik-backend/src/test/java/com/comet/opik/api/resources/v1/priv/DatasetsResourceTest.java b/apps/opik-backend/src/test/java/com/comet/opik/api/resources/v1/priv/DatasetsResourceTest.java
index 9b6a410847..d6c7df2fa8 100644
--- a/apps/opik-backend/src/test/java/com/comet/opik/api/resources/v1/priv/DatasetsResourceTest.java
+++ b/apps/opik-backend/src/test/java/com/comet/opik/api/resources/v1/priv/DatasetsResourceTest.java
@@ -1197,6 +1197,22 @@ void create__success() {
createAndAssert(dataset);
}
+ @Test
+ @DisplayName("when description is multiline, then accept the request")
+ void create__whenDescriptionIsMultiline__thenAcceptTheRequest() {
+
+ var dataset = factory.manufacturePojo(Dataset.class).toBuilder()
+ .id(null)
+ .description("""
+ Test
+ Description
+ """
+ )
+ .build();
+
+ createAndAssert(dataset);
+ }
+
@Test
@DisplayName("when creating datasets with same name in different workspaces, then accept the request")
void create__whenCreatingDatasetsWithSameNameInDifferentWorkspaces__thenAcceptTheRequest() {
diff --git a/apps/opik-backend/src/test/java/com/comet/opik/api/resources/v1/priv/ProjectsResourceTest.java b/apps/opik-backend/src/test/java/com/comet/opik/api/resources/v1/priv/ProjectsResourceTest.java
index ea8b879d8a..193909b6ef 100644
--- a/apps/opik-backend/src/test/java/com/comet/opik/api/resources/v1/priv/ProjectsResourceTest.java
+++ b/apps/opik-backend/src/test/java/com/comet/opik/api/resources/v1/priv/ProjectsResourceTest.java
@@ -854,6 +854,30 @@ void create__whenWorkspaceNameIsSpecified__thenAcceptTheRequest() {
assertProject(project.toBuilder().id(id).build(), apiKey, workspaceName);
}
+ @Test
+ @DisplayName("when workspace description is multiline, then accept the request")
+ void create__whenDescriptionIsMultiline__thenAcceptTheRequest() {
+ var project = factory.manufacturePojo(Project.class);
+
+ project = project.toBuilder().description("Test Project\n\nMultiline Description").build();
+
+ UUID id;
+ try (var actualResponse = client.target(URL_TEMPLATE.formatted(baseURI)).request()
+ .accept(MediaType.APPLICATION_JSON_TYPE)
+ .header(HttpHeaders.AUTHORIZATION, API_KEY)
+ .header(WORKSPACE_HEADER, TEST_WORKSPACE)
+ .post(Entity.json(project))) {
+
+ assertThat(actualResponse.getStatusInfo().getStatusCode()).isEqualTo(201);
+ assertThat(actualResponse.hasEntity()).isFalse();
+ assertThat(actualResponse.getHeaderString("Location")).matches(Pattern.compile(URL_PATTERN));
+
+ id = TestUtils.getIdFromLocation(actualResponse.getLocation());
+ }
+
+ assertProject(project.toBuilder().id(id).build());
+ }
+
@Test
@DisplayName("when description is null, then accept the request")
void create__whenDescriptionIsNull__thenAcceptNameCreate() {
diff --git a/apps/opik-backend/src/test/java/com/comet/opik/utils/ValidationUtilsTest.java b/apps/opik-backend/src/test/java/com/comet/opik/utils/ValidationUtilsTest.java
new file mode 100644
index 0000000000..f0b2f978c8
--- /dev/null
+++ b/apps/opik-backend/src/test/java/com/comet/opik/utils/ValidationUtilsTest.java
@@ -0,0 +1,30 @@
+package com.comet.opik.utils;
+
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
+
+import java.util.stream.Stream;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+class ValidationUtilsTest {
+
+ public static Stream testNullOrNotBlank() {
+ return Stream.of(
+ Arguments.of("", false),
+ Arguments.of(" ", false),
+ Arguments.of("\n", false),
+ Arguments.of("a", true),
+ Arguments.of(" a ", true),
+ Arguments.of("\n a \n", true)
+ );
+ }
+
+ @ParameterizedTest
+ @MethodSource
+ void testNullOrNotBlank(String input, boolean expected) {
+ assertEquals(expected, input.matches(ValidationUtils.NULL_OR_NOT_BLANK));
+ }
+
+}
\ No newline at end of file