fix(tagLabel): lenient appliedAt date deserialization#27771
fix(tagLabel): lenient appliedAt date deserialization#27771Khairajani merged 15 commits intomainfrom
Conversation
|
Hi there 👋 Thanks for your contribution! The OpenMetadata team will review the PR shortly! Once it has been labeled as Let us know if you need any help! |
Accept ISO-8601 datetimes with or without fractional seconds for
TagLabel.appliedAt. Python clients omit fractional when a datetime's
microsecond is zero, emitting strings like "2026-04-24T10:27:06Z" that
the strict global SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSSSS'Z'")
rejects, causing PATCH operations to fail with "Failed to convert
JsonValue to target class".
22c5e27 to
5788f98
Compare
🟡 Playwright Results — all passed (18 flaky)✅ 3980 passed · ❌ 0 failed · 🟡 18 flaky · ⏭️ 86 skipped
🟡 18 flaky test(s) (passed on retry)
How to debug locally# Download playwright-test-results-<shard> artifact and unzip
npx playwright show-trace path/to/trace.zip # view trace |
Instant.parse misreads the server's own SimpleDateFormat output (SSSSSS = left-padded ms, e.g. ".000918Z"), collapsing ms to 0 on every PATCH round-trip and breaking tag mutations end-to-end. Detect that form by its 6-digit "000"-leading fractional and parse the digits as left-padded ms; fall through to Instant.parse for no-fractional and Pydantic microsecond inputs.
…put forms The previous iteration replaced Jackson's default Date deserializer with Instant.parse, which silently dropped support for JSON numbers and numeric strings — wire forms that arrive in JSON Patch payloads. Tag PATCHes started 4xx-ing on every entity in CI. Delegate to ctxt.parseDate (the same global SimpleDateFormat path the rest of Jackson uses) so every form that worked pre-PR keeps working. Numeric tokens and stringified epoch-ms restore Jackson's prior tolerance. The only addition is bare-second padding: if the value is "…ssZ" (Pydantic with microsecond=0, the original PR goal), pad with .000000 so the global format accepts it.
Per gitar-bot review. Avoids misinterpreting short all-digit strings (e.g. compact "YYYYMMDD") as epoch milliseconds. Modern dates are 13 digits; 10 is the threshold for any year ≥ 2001.
Code Review ✅ Approved 2 resolved / 2 findingsSimplifies TagLabel.appliedAt deserialization to support both ISO formats and epoch-millisecond inputs, resolving potential failures when serializing datetimes without fractional seconds. No issues found. ✅ 2 resolved✅ Edge Case: LenientIsoDateDeserializer doesn't wrap DateTimeParseException
✅ Edge Case: looksLikeEpochMillis has no length guard against ambiguous inputs
OptionsDisplay: compact → Showing less information. Comment with these commands to change:
Was this helpful? React with 👍 / 👎 | Gitar |
|



Summary
TagLabel.appliedAtis typedjava.util.Dateand the global JacksonObjectMapperis configured withSimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSSSS'Z'")— which strictly requires 6-digit fractional seconds on parse. Python clients using Pydantic emit datetimes viadatetime.isoformat(), which omits the fractional part entirely whenmicrosecond == 0(~1/1000 odds per timestamp). The result is a payload like"2026-04-24T10:27:06Z"that the server cannot deserialize, surfacing as:This was causing intermittent automator propagation failures in CI — every PATCH carrying a round-tripped
appliedAtwithmicrosecond=0was being rejected. Same class of bug as thetag_usage.appliedAtprecision fix in the 1.11.8 / 1.12.0 MySQL migrations, just on the wire side.Fix
Register a Jackson mixin on
TagLabel.appliedAtthat wraps Jackson's normal Date path with one tiny pre-processing step: if the incoming value is the bare-second ISO form (yyyy-MM-ddTHH:mm:ssZ), pad the fractional with.000000so the globalSimpleDateFormataccepts it. Otherwise the value is handed toDeserializationContext#parseDate— the same path Jackson uses for every other Date field — so every wire form that worked before keeps working.Cases the deserializer handles:
"2026-04-24T10:27:06Z"→ padded → SDF parses as ms=0 (this PR's goal)"2026-04-24T10:27:06.000918Z"→ SDF parses as ms=918 (round-trip preservation)"2026-04-24T10:27:06.918Z"→ SDF lenient → ms=918 (JSDate.toISOString())1777976050918→new Date(getLongValue())(matches Jackson default)"1777976050918"(10–19 digits) →Date(Long.parseLong)(JSON-Patch hop sometimes stringifies numbers)nullorJsonMappingExceptionviahandleWeirdStringValueMixin is added to the global
OBJECT_MAPPERbefore any.copy(), so all five derived mappers inherit it. No generated code is touched. Serialization is unchanged (still always emits 6-digit fractional from the global format).Test plan
JsonUtilsTest#testTagLabelAppliedAtAcceptsBareSecondPrecision— original PR goalJsonUtilsTest#testTagLabelAppliedAtRoundTripPreservesMillis— server's own.000918Zround-trip preserves msJsonUtilsTest#testTagLabelAppliedAtAcceptsEpochMillis— JSON number + numeric string formsJsonUtilsTest#testTagLabelAppliedAtMalformedRaisesMappingException— error path surfaces asJsonMappingExceptionmvn -pl openmetadata-spec install -DskipTestspassesmvn -pl openmetadata-service test -Dtest=JsonUtilsTest— 14/14 greenmvn spotless:applyclean