Add custom Jackson deserializer to handle empty plugin configs and re…#6598
Add custom Jackson deserializer to handle empty plugin configs and re…#6598Davidding4718 wants to merge 3 commits intoopensearch-project:mainfrom
Conversation
…ject empty strings Signed-off-by: Siqi Ding <dingdd@amazon.com>
1427075 to
4cee36f
Compare
5336805 to
2792906
Compare
oeyh
left a comment
There was a problem hiding this comment.
Thanks for working on this! A few comments below:
| 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) | ||
| } |
There was a problem hiding this comment.
Does this static block change anything from the original:
private static final ObjectMapper SERIALIZER_OBJECT_MAPPER = new ObjectMapper();
There was a problem hiding this comment.
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")); |
| JsonMappingException.class, | ||
| () -> mapper.readValue(yaml, PluginModel.class) | ||
| ); | ||
| assertThat(exception.getMessage(), org.hamcrest.Matchers.containsString("String values not allowed")); |
| JsonMappingException.class, | ||
| () -> objectMapper.readValue(inputStream, PipelinesDataFlowModel.class) | ||
| ); | ||
| assertThat(exception.getMessage(), org.hamcrest.Matchers.containsString("Empty string is not allowed")); |
| @@ -1,2 +1,2 @@ | |||
| --- | |||
| customPlugin: | |||
| customPlugin: {} | |||
There was a problem hiding this comment.
Should we add new tests with {}, instead of changing the previous test? Otherwise, null case is no longer tested.
Similar for other tests.
There was a problem hiding this comment.
Yea, I think we should keep this file the way it was.
| 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) | ||
| } |
There was a problem hiding this comment.
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( |
There was a problem hiding this comment.
Another place to use a static import.
| objectMapper = new ObjectMapper(new YAMLFactory()); | ||
| } | ||
|
|
||
| // --- Valid pipeline scenarios --- |
There was a problem hiding this comment.
Please don't have sections like this. They don't remain over time. There are two approaches:
- You can use
@Nestedto group them, but I tend to avoid@Nestedunless there is common setup. - Just remove this comment.
There was a problem hiding this comment.
Thanks! Will remove it in new commit
| assertThat(pipeline.getSinks().get(0).getPluginSettings().size(), equalTo(0)); | ||
| } | ||
|
|
||
| // --- Invalid pipeline scenario --- |
There was a problem hiding this comment.
Thanks! Will remove it in new commit
| @@ -1,2 +1,2 @@ | |||
| --- | |||
| customPlugin: | |||
| customPlugin: {} | |||
There was a problem hiding this comment.
Yea, I think we should keep this file the way it was.
| test-pipeline: | ||
| source: | ||
| testSource: null | ||
| testSource: {} |
There was a problem hiding this comment.
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)); |
There was a problem hiding this comment.
Do we need this feature enabled? enable(YAMLGenerator.Feature.USE_PLATFORM_LINE_BREAKS)
2792906 to
9fd97de
Compare
✅ License Header Check PassedAll newly added files have proper license headers. Great work! 🎉 |
| @@ -1,2 +1,2 @@ | |||
| --- | |||
| customPlugin: null | |||
| customPlugin: {} | |||
There was a problem hiding this comment.
This should be null. There's another file below for {}.
| throw context.weirdStringException(value, Map.class, | ||
| "String values not allowed for plugin '" + pluginName + "'"); | ||
| } | ||
| } |
There was a problem hiding this comment.
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: nullThere was a problem hiding this comment.
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); |
There was a problem hiding this comment.
Can you clarify why this serialization and immediate deserialization is necessary here and add a comment above the code?
There was a problem hiding this comment.
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>
9fd97de to
ff519f6
Compare
Signed-off-by: Siqi Ding <dingdd@amazon.com>
e81d6c8 to
846416a
Compare
Description
After upgrading Jackson, empty YAML plugin configurations like
stdout:were being deserialized as empty strings""instead of null/empty objects, causingMismatchedInputException. This change adds a custom Jackson deserializer toPluginModelthat 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
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.