Skip to content

Add custom Jackson deserializer to handle empty plugin configs and re…#6598

Open
Davidding4718 wants to merge 3 commits intoopensearch-project:mainfrom
Davidding4718:jackson-update-serailization
Open

Add custom Jackson deserializer to handle empty plugin configs and re…#6598
Davidding4718 wants to merge 3 commits intoopensearch-project:mainfrom
Davidding4718:jackson-update-serailization

Conversation

@Davidding4718
Copy link
Copy Markdown
Contributor

Description

After upgrading Jackson, empty YAML plugin configurations like stdout: were being deserialized as empty strings "" instead of null/empty objects, causing MismatchedInputException. This change adds a custom Jackson deserializer to PluginModel that explicitly handles the valid formats (null, empty value, {}) and rejects empty strings "" with a clear error message. The custom serializer is also updated to output {} for empty (non-null) plugin settings.

Issues Resolved

NULL

Check List

  • New functionality includes testing.
  • New functionality has a documentation issue. Please link to it in this PR.
    • New functionality has javadoc added
  • Commits are signed with a real name per the DCO

By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license.
For more information on following Developer Certificate of Origin and signing off your commits, please check here.

…ject empty strings

Signed-off-by: Siqi Ding <dingdd@amazon.com>
@Davidding4718 Davidding4718 force-pushed the jackson-update-serailization branch from 1427075 to 4cee36f Compare March 3, 2026 16:17
@Davidding4718 Davidding4718 marked this pull request as draft March 3, 2026 19:20
@Davidding4718 Davidding4718 marked this pull request as ready for review March 3, 2026 20:43
@dlvenable dlvenable added this to the v2.15 milestone Mar 3, 2026
@Davidding4718 Davidding4718 force-pushed the jackson-update-serailization branch 2 times, most recently from 5336805 to 2792906 Compare March 10, 2026 17:35
Copy link
Copy Markdown
Collaborator

@oeyh oeyh left a comment

Choose a reason for hiding this comment

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

Thanks for working on this! A few comments below:

Comment on lines +43 to +47
static {
SERIALIZER_OBJECT_MAPPER = new ObjectMapper();
// Note: We don't configure coercion here because our custom deserializer
// handles all the cases (null, empty, {}, and rejects empty strings)
}
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Does this static block change anything from the original:

private static final ObjectMapper SERIALIZER_OBJECT_MAPPER = new ObjectMapper();

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Yea, revert this small section.

I think you are working on code that I originally experimented with. I once had some configurations on the ObjectMapper that are no longer relevant.

JsonMappingException.class,
() -> mapper.readValue(inputStream, PluginModel.class)
);
assertThat(exception.getMessage(), org.hamcrest.Matchers.containsString("Empty string is not allowed"));
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Use static import instead.

JsonMappingException.class,
() -> mapper.readValue(yaml, PluginModel.class)
);
assertThat(exception.getMessage(), org.hamcrest.Matchers.containsString("String values not allowed"));
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Use static import instead.

JsonMappingException.class,
() -> objectMapper.readValue(inputStream, PipelinesDataFlowModel.class)
);
assertThat(exception.getMessage(), org.hamcrest.Matchers.containsString("Empty string is not allowed"));
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Same here.

@@ -1,2 +1,2 @@
---
customPlugin:
customPlugin: {}
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Should we add new tests with {}, instead of changing the previous test? Otherwise, null case is no longer tested.
Similar for other tests.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Yea, I think we should keep this file the way it was.

Copy link
Copy Markdown
Member

@dlvenable dlvenable left a comment

Choose a reason for hiding this comment

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

Thank you @Davidding4718 !

Comment on lines +43 to +47
static {
SERIALIZER_OBJECT_MAPPER = new ObjectMapper();
// Note: We don't configure coercion here because our custom deserializer
// handles all the cases (null, empty, {}, and rejects empty strings)
}
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Yea, revert this small section.

I think you are working on code that I originally experimented with. I once had some configurations on the ObjectMapper that are no longer relevant.

final InputStream inputStream = PluginModelTests.class.getResourceAsStream("plugin_model_empty_string.yaml");
final ObjectMapper mapper = new ObjectMapper(new YAMLFactory());

final JsonMappingException exception = org.junit.jupiter.api.Assertions.assertThrows(
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Another place to use a static import.

objectMapper = new ObjectMapper(new YAMLFactory());
}

// --- Valid pipeline scenarios ---
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Please don't have sections like this. They don't remain over time. There are two approaches:

  1. You can use @Nested to group them, but I tend to avoid @Nested unless there is common setup.
  2. Just remove this comment.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Thanks! Will remove it in new commit

assertThat(pipeline.getSinks().get(0).getPluginSettings().size(), equalTo(0));
}

// --- Invalid pipeline scenario ---
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Again, handle this comment.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Thanks! Will remove it in new commit

@@ -1,2 +1,2 @@
---
customPlugin:
customPlugin: {}
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Yea, I think we should keep this file the way it was.

test-pipeline:
source:
testSource: null
testSource: {}
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Do we need to modify this file?

I think the only scenario where exiting inputs will no longer pass is when we do a deserialize followed by a serialize.

Maybe a better way to test this would be to perform another deserialize and then compare the maps?

@Test
final void testRoundTrip_withNullValue() throws IOException {
final InputStream inputStream = PluginModelTests.class.getResourceAsStream("plugin_model_with_null.yaml");
final ObjectMapper mapper = new ObjectMapper(new YAMLFactory().enable(YAMLGenerator.Feature.USE_PLATFORM_LINE_BREAKS));
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Do we need this feature enabled? enable(YAMLGenerator.Feature.USE_PLATFORM_LINE_BREAKS)

@Davidding4718 Davidding4718 force-pushed the jackson-update-serailization branch from 2792906 to 9fd97de Compare March 24, 2026 20:08
@github-actions
Copy link
Copy Markdown

github-actions bot commented Mar 24, 2026

✅ License Header Check Passed

All newly added files have proper license headers. Great work! 🎉

@@ -1,2 +1,2 @@
---
customPlugin: null
customPlugin: {}
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

This should be null. There's another file below for {}.

throw context.weirdStringException(value, Map.class,
"String values not allowed for plugin '" + pluginName + "'");
}
}
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

No else branch here. Can you check what will happen if jsonParser.currentToken() is not START_OBJECT or VALUE_NULL or VALUE_STRING. For example,:

  test-pipeline:
    source:                                                                                                                                                                                                                                                                                                          
      stdin: 123 # or true/false, or array [1, 2]                                                                                                                                                                                                                                                                                                     
    processor:                                                                                                                                                                                                                                                                                                       
    - uppercase_string: null
    sink:
    - stdout: null

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Good point! Will add a else condition with following unit test.

gen.writeObjectField(value.getPluginName(), serializedInner);

final String jsonString = SERIALIZER_OBJECT_MAPPER.writeValueAsString(value.innerModel);
final Map<String, Object> serializedInner = SERIALIZER_OBJECT_MAPPER.readValue(jsonString, Map.class);
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Can you clarify why this serialization and immediate deserialization is necessary here and add a comment above the code?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Added a comment in the code. The round-trip is needed because innerModel can be a subclass (e.g. SinkInternalJsonModel) with extra Jackson-annotated fields. Serializing to JSON and reading back as a Map lets Jackson collect all those fields together, so we can check if the result is empty and write it out correctly. Directly accessing pluginSettings would miss any extra fields defined on subclasses.

Signed-off-by: Siqi Ding <dingdd@amazon.com>
@Davidding4718 Davidding4718 force-pushed the jackson-update-serailization branch from 9fd97de to ff519f6 Compare March 27, 2026 20:39
Signed-off-by: Siqi Ding <dingdd@amazon.com>
@Davidding4718 Davidding4718 force-pushed the jackson-update-serailization branch from e81d6c8 to 846416a Compare March 27, 2026 21:35
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants