Skip to content
Open
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
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
import org.openmetadata.schema.services.connections.dashboard.TableauConnection;
import org.openmetadata.schema.services.connections.database.MysqlConnection;
import org.openmetadata.schema.services.connections.database.common.basicAuth;
import org.openmetadata.schema.type.TagLabel;
import org.openmetadata.schema.utils.JsonUtils;

/** This test provides examples of how to use applyPatch */
Expand Down Expand Up @@ -272,4 +273,38 @@

assertEquals("New Display Name", result.getDisplayName());
}

/**
* Python clients drop fractional seconds when a datetime's microsecond is 0,
* sending "…ssZ" instead of "…ss.SSSSSSZ". The strict global SimpleDateFormat
* rejects that form. Verify the lenient mixin on TagLabel.appliedAt accepts both.
*/
@Test
void testTagLabelAppliedAtAcceptsBareSecondPrecision() {
String withoutFractional = "{\"tagFQN\":\"x.y\",\"appliedAt\":\"2026-04-24T10:27:06Z\"}";
String withMicros = "{\"tagFQN\":\"x.y\",\"appliedAt\":\"2026-04-24T10:27:06.918000Z\"}";

TagLabel bare = JsonUtils.readValue(withoutFractional, TagLabel.class);
TagLabel withFrac = JsonUtils.readValue(withMicros, TagLabel.class);

assertEquals(0L, bare.getAppliedAt().getTime() % 1000, "bare-second form parses to ms=0");
assertEquals(918L, withFrac.getAppliedAt().getTime() % 1000, "fractional form preserves ms");
assertEquals(
918L,
withFrac.getAppliedAt().getTime() - bare.getAppliedAt().getTime(),
"both forms parse the same second");
}

/** Malformed ISO strings should surface as JsonMappingException with the JSON path. */
@Test
void testTagLabelAppliedAtMalformedRaisesMappingException() {
String malformed = "{\"tagFQN\":\"x.y\",\"appliedAt\":\"not-a-date\"}";
com.fasterxml.jackson.databind.JsonMappingException ex =
assertThrows(
com.fasterxml.jackson.databind.JsonMappingException.class,
() -> JsonUtils.readValue(malformed, TagLabel.class));

Check failure on line 305 in openmetadata-service/src/test/java/org/openmetadata/service/util/JsonUtilsTest.java

View workflow job for this annotation

GitHub Actions / Test Report (postgresql)

JsonUtilsTest.testTagLabelAppliedAtMalformedRaisesMappingException

Unexpected exception type thrown, expected: <com.fasterxml.jackson.databind.JsonMappingException> but was: <org.openmetadata.schema.exception.JsonParsingException>
Raw output
org.opentest4j.AssertionFailedError: Unexpected exception type thrown, expected: <com.fasterxml.jackson.databind.JsonMappingException> but was: <org.openmetadata.schema.exception.JsonParsingException>
	at org.junit.jupiter.api.AssertionFailureBuilder.build(AssertionFailureBuilder.java:151)
	at org.junit.jupiter.api.AssertThrows.assertThrows(AssertThrows.java:67)
	at org.junit.jupiter.api.AssertThrows.assertThrows(AssertThrows.java:35)
	at org.junit.jupiter.api.Assertions.assertThrows(Assertions.java:3128)
	at org.openmetadata.service.util.JsonUtilsTest.testTagLabelAppliedAtMalformedRaisesMappingException(JsonUtilsTest.java:303)
	at java.base/java.lang.reflect.Method.invoke(Method.java:580)
	at java.base/java.util.ArrayList.forEach(ArrayList.java:1596)
	at java.base/java.util.ArrayList.forEach(ArrayList.java:1596)
Caused by: org.openmetadata.schema.exception.JsonParsingException: JSON parsing failed with message [Failed to process JSON ].
	at org.openmetadata.schema.utils.JsonUtils.readValue(JsonUtils.java:247)
	at org.openmetadata.service.util.JsonUtilsTest.lambda$testTagLabelAppliedAtMalformedRaisesMappingException$6(JsonUtilsTest.java:305)
	at org.junit.jupiter.api.AssertThrows.assertThrows(AssertThrows.java:53)
	... 6 more
Caused by: com.fasterxml.jackson.databind.exc.InvalidFormatException: Cannot deserialize value of type `java.util.Date` from String "not-a-date": Expected ISO-8601 date-time: Text 'not-a-date' could not be parsed at index 0
 at [Source: REDACTED (`StreamReadFeature.INCLUDE_SOURCE_IN_LOCATION` disabled); line: 1, column: 29] (through reference chain: org.openmetadata.schema.type.TagLabel["appliedAt"])
	at com.fasterxml.jackson.databind.exc.InvalidFormatException.from(InvalidFormatException.java:67)
	at com.fasterxml.jackson.databind.DeserializationContext.weirdStringException(DeserializationContext.java:1959)
	at com.fasterxml.jackson.databind.DeserializationContext.handleWeirdStringValue(DeserializationContext.java:1245)
	at org.openmetadata.schema.utils.JsonUtils$LenientIsoDateDeserializer.deserialize(JsonUtils.java:922)
	at org.openmetadata.schema.utils.JsonUtils$LenientIsoDateDeserializer.deserialize(JsonUtils.java:911)
	at com.fasterxml.jackson.databind.deser.impl.MethodProperty.deserializeAndSet(MethodProperty.java:129)
	at com.fasterxml.jackson.databind.deser.BeanDeserializer.deserialize(BeanDeserializer.java:273)
	at com.fasterxml.jackson.module.blackbird.deser.SuperSonicBeanDeserializer.deserialize(SuperSonicBeanDeserializer.java:155)
	at com.fasterxml.jackson.databind.deser.DefaultDeserializationContext.readRootValue(DefaultDeserializationContext.java:342)
	at com.fasterxml.jackson.databind.ObjectMapper._readMapAndClose(ObjectMapper.java:4939)
	at com.fasterxml.jackson.databind.ObjectMapper.readValue(ObjectMapper.java:3876)
	at com.fasterxml.jackson.databind.ObjectMapper.readValue(ObjectMapper.java:3844)
	at org.openmetadata.schema.utils.JsonUtils.readValue(JsonUtils.java:245)
	... 8 more

Check failure on line 305 in openmetadata-service/src/test/java/org/openmetadata/service/util/JsonUtilsTest.java

View workflow job for this annotation

GitHub Actions / Test Report (mysql)

JsonUtilsTest.testTagLabelAppliedAtMalformedRaisesMappingException

Unexpected exception type thrown, expected: <com.fasterxml.jackson.databind.JsonMappingException> but was: <org.openmetadata.schema.exception.JsonParsingException>
Raw output
org.opentest4j.AssertionFailedError: Unexpected exception type thrown, expected: <com.fasterxml.jackson.databind.JsonMappingException> but was: <org.openmetadata.schema.exception.JsonParsingException>
	at org.junit.jupiter.api.AssertionFailureBuilder.build(AssertionFailureBuilder.java:151)
	at org.junit.jupiter.api.AssertThrows.assertThrows(AssertThrows.java:67)
	at org.junit.jupiter.api.AssertThrows.assertThrows(AssertThrows.java:35)
	at org.junit.jupiter.api.Assertions.assertThrows(Assertions.java:3128)
	at org.openmetadata.service.util.JsonUtilsTest.testTagLabelAppliedAtMalformedRaisesMappingException(JsonUtilsTest.java:303)
	at java.base/java.lang.reflect.Method.invoke(Method.java:580)
	at java.base/java.util.ArrayList.forEach(ArrayList.java:1596)
	at java.base/java.util.ArrayList.forEach(ArrayList.java:1596)
Caused by: org.openmetadata.schema.exception.JsonParsingException: JSON parsing failed with message [Failed to process JSON ].
	at org.openmetadata.schema.utils.JsonUtils.readValue(JsonUtils.java:247)
	at org.openmetadata.service.util.JsonUtilsTest.lambda$testTagLabelAppliedAtMalformedRaisesMappingException$6(JsonUtilsTest.java:305)
	at org.junit.jupiter.api.AssertThrows.assertThrows(AssertThrows.java:53)
	... 6 more
Caused by: com.fasterxml.jackson.databind.exc.InvalidFormatException: Cannot deserialize value of type `java.util.Date` from String "not-a-date": Expected ISO-8601 date-time: Text 'not-a-date' could not be parsed at index 0
 at [Source: REDACTED (`StreamReadFeature.INCLUDE_SOURCE_IN_LOCATION` disabled); line: 1, column: 29] (through reference chain: org.openmetadata.schema.type.TagLabel["appliedAt"])
	at com.fasterxml.jackson.databind.exc.InvalidFormatException.from(InvalidFormatException.java:67)
	at com.fasterxml.jackson.databind.DeserializationContext.weirdStringException(DeserializationContext.java:1959)
	at com.fasterxml.jackson.databind.DeserializationContext.handleWeirdStringValue(DeserializationContext.java:1245)
	at org.openmetadata.schema.utils.JsonUtils$LenientIsoDateDeserializer.deserialize(JsonUtils.java:922)
	at org.openmetadata.schema.utils.JsonUtils$LenientIsoDateDeserializer.deserialize(JsonUtils.java:911)
	at com.fasterxml.jackson.databind.deser.impl.MethodProperty.deserializeAndSet(MethodProperty.java:129)
	at com.fasterxml.jackson.databind.deser.BeanDeserializer.deserialize(BeanDeserializer.java:273)
	at com.fasterxml.jackson.module.blackbird.deser.SuperSonicBeanDeserializer.deserialize(SuperSonicBeanDeserializer.java:155)
	at com.fasterxml.jackson.databind.deser.DefaultDeserializationContext.readRootValue(DefaultDeserializationContext.java:342)
	at com.fasterxml.jackson.databind.ObjectMapper._readMapAndClose(ObjectMapper.java:4939)
	at com.fasterxml.jackson.databind.ObjectMapper.readValue(ObjectMapper.java:3876)
	at com.fasterxml.jackson.databind.ObjectMapper.readValue(ObjectMapper.java:3844)
	at org.openmetadata.schema.utils.JsonUtils.readValue(JsonUtils.java:245)
	... 8 more
assertTrue(
ex.getMessage().contains("appliedAt") || ex.getMessage().contains("ISO-8601"),
"error should mention the field or expected format: " + ex.getMessage());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,18 @@
package org.openmetadata.schema.utils;

import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.StreamReadConstraints;
import com.fasterxml.jackson.core.StreamReadFeature;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.JsonDeserializer;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.fasterxml.jackson.databind.json.JsonMapper;
import com.fasterxml.jackson.databind.node.JsonNodeFactory;
import com.fasterxml.jackson.databind.node.ObjectNode;
Expand Down Expand Up @@ -50,10 +54,13 @@
import java.nio.file.Paths;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.time.Instant;
import java.time.format.DateTimeParseException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.HashSet;
Expand All @@ -80,6 +87,7 @@
import org.openmetadata.schema.entity.Type;
import org.openmetadata.schema.entity.type.Category;
import org.openmetadata.schema.exception.JsonParsingException;
import org.openmetadata.schema.type.TagLabel;

@Slf4j
public final class JsonUtils {
Expand Down Expand Up @@ -123,6 +131,11 @@ public final class JsonUtils {
// Java 21 optimized introspection/accessors for faster convertValue/read/write paths.
OBJECT_MAPPER.registerModule(new BlackbirdModule());

// Accept TagLabel.appliedAt with or without fractional seconds. Python clients
// serialize datetimes with microsecond=0 as "…ssZ" (no fractional), which the
// strict global SimpleDateFormat("…SSSSSS'Z'") rejects.
OBJECT_MAPPER.addMixIn(TagLabel.class, TagLabelDateMixin.class);

// Lenient ObjectMapper to ignore unknown properties
OBJECT_MAPPER_LENIENT = OBJECT_MAPPER.copy();
OBJECT_MAPPER_LENIENT.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
Expand Down Expand Up @@ -887,4 +900,33 @@ private static Collection<String> getResourcesFromJarFile(File file, Pattern pat
}
return retval;
}

/**
* Lenient deserializer for ISO-8601 instants. Accepts strings with or without
* fractional seconds, e.g. "2026-04-24T10:27:06Z" and "2026-04-24T10:27:06.918000Z".
* Java's {@link SimpleDateFormat} configured globally with pattern "…SSSSSS'Z'"
* rejects the no-fractional form, which Python clients can emit when a datetime's
* microsecond is 0.
*/
public static final class LenientIsoDateDeserializer extends JsonDeserializer<Date> {
@Override
public Date deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
String value = p.getValueAsString();
if (value == null || value.isEmpty()) {
return null;
}
try {
return Date.from(Instant.parse(value));
} catch (DateTimeParseException e) {
return (Date)
ctxt.handleWeirdStringValue(
Date.class, value, "Expected ISO-8601 date-time: %s", e.getMessage());
}
}
Comment thread
gitar-bot[bot] marked this conversation as resolved.
}

abstract static class TagLabelDateMixin {
@JsonDeserialize(using = LenientIsoDateDeserializer.class)
Date appliedAt;
}
}
Loading