[Fix][connector-http] fix parsing httpjson, the number of two fields is inconsistent with the import failure#9103
Conversation
There was a problem hiding this comment.
Pull Request Overview
This PR fixes the JSON parsing issue in the HTTP connector by ensuring that inconsistent field counts across JSON paths are handled gracefully. Instead of throwing an exception when field sizes differ, the code now calculates the maximum field count and pads shorter lists with null values.
- Removed strict size equality check across parsed JSON results.
- Introduced dynamic null-padding to standardize the size of each result list.
- Updated log messaging to warn users about padded fields.
Comments suppressed due to low confidence (1)
seatunnel-connectors-v2/connector-http/connector-http-base/src/main/java/org/apache/seatunnel/connectors/seatunnel/http/source/HttpSourceReader.java:273
- [nitpick] Consider combining the message string into a single literal with placeholders instead of using string concatenation, which improves readability and minimizes potential formatting issues.
"Field [{}] with size ({}) is less than max size ({}), will be padded with null values. " + "This may happen when JSON paths return different numbers of elements.",
|
Please add test cases |
| } | ||
| source{ | ||
| Http { | ||
| url = "https://api-seller.ozon.ru/v2/finance/realization" |
There was a problem hiding this comment.
user mockserver mock api & response
https://github.com/apache/seatunnel/blob/dev/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-http-e2e/src/test/resources/github_json_to_assert.conf#L26
https://github.com/apache/seatunnel/blob/dev/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-http-e2e/src/test/resources/mockserver-config.json#L430
Hisoka-X
left a comment
There was a problem hiding this comment.
We need add new option to change the behavior.
There was a problem hiding this comment.
Could you add a unit test for https://github.com/apache/seatunnel/pull/9103/files#diff-37ec4d574545a62fd6623d950e8ba947c72116663ada3424cd673ffbe2a733daL214 ?
| if (httpParameter.isJsonFiledMissedReturnNull()) { | ||
| int maxLength = 0; | ||
| for (List<?> result : results) { | ||
| maxLength = Math.max(maxLength, result.size()); | ||
| } | ||
| for (int i = 0; i < results.size(); i++) { | ||
| List<String> result = results.get(i); | ||
| if (result.size() < maxLength) { | ||
| log.warn( | ||
| "Field [{}] with size ({}) is less than max size ({}), will be padded with null values. " | ||
| + "This may happen when JSON paths return different numbers of elements.", | ||
| jsonPaths[i].getPath(), | ||
| result.size(), | ||
| maxLength); | ||
| for (int j = result.size(); j < maxLength; j++) { | ||
| result.add(null); | ||
| } | ||
| } | ||
| } | ||
| } else { |
There was a problem hiding this comment.
This solution will cause the data to be out of order. For example:
[{
"key1":"value11",
"key2":"value22"
},
{
"key2":"value33"
"key3":"value44"
},
{
"key1":"value55"
"key3":"value66"
}
]Json Fields:
json_field = {
key1 = "$[*].key1"
key2 = "$[*].key2"
}The expected results should be:
| key1 | key2 |
|---|---|
| value11 | value22 |
| null | value33 |
| value55 | null |
But current logic of results is:
| key1 | key2 |
|---|---|
| value11 | value22 |
| value55 | value33 |
The current implementation just wants to add null at the end, but does not accurately locate which data is null.
There was a problem hiding this comment.
Yes, it was indeed an issue and I didn't take into account the data correspondence, now I fixed it.
Hisoka-X
left a comment
There was a problem hiding this comment.
Also please check why Option.DEFAULT_PATH_LEAF_TO_NULL not worked. If it work well. The result size should always equals. https://github.com/apache/seatunnel/pull/9103/files#diff-37ec4d574545a62fd6623d950e8ba947c72116663ada3424cd673ffbe2a733daL64
| | common-options | | No | - | Source plugin common parameters, please refer to [Source Common Options](../source-common-options.md) for details | | ||
| | keep_params_as_form | Boolean | No | false | Whether the params are submitted according to the form, used for compatibility with legacy behaviors. When true, the value of the params parameter is submitted through the form. | | ||
| | keep_page_param_as_http_param | Boolean | No | false | Whether to set the paging parameters to params. For compatibility with legacy behaviors. | | ||
| | json_filed_missed_return_null | Boolean | No | false | When the json field is missing, set true return null else error. |
There was a problem hiding this comment.
| | json_filed_missed_return_null | Boolean | No | false | When the json field is missing, set true return null else error. | |
| | json_filed_missed_return_null | Boolean | No | false | When the json field is missing, set true return null else error. | |
|
|
||
| Container.ExecResult execResult21 = container.executeJob("/http_to_console.conf"); | ||
| Assertions.assertEquals(0, execResult21.getExitCode()); |
There was a problem hiding this comment.
I perfer use unit test to check it rather than e2e. Unit test can check all return value more easier. Current e2e test case you added even not check result data.
| <dependency> | ||
| <groupId>cn.hutool</groupId> | ||
| <artifactId>hutool-json</artifactId> | ||
| <version>${hutool-json.version}</version> | ||
| <scope>test</scope> |
There was a problem hiding this comment.
Please do not introduce new json dependency. Just use jackson enough.
|
|
||
| Container.ExecResult execResult21 = container.executeJob("/http_to_console.conf"); | ||
| Assertions.assertEquals(0, execResult21.getExitCode()); |
| if (httpParameter.isJsonFiledMissedReturnNull()) { | ||
| // Extract the common parent path from all configured paths | ||
| String commonParentPath = extractCommonParentPath(jsonPaths); | ||
| if (commonParentPath == null) { | ||
| throw new HttpConnectorException( | ||
| HttpConnectorErrorCode.FIELD_DATA_IS_INCONSISTENT, | ||
| "Could not find common parent path in JsonPaths. All paths must share a common array parent."); | ||
| } | ||
|
|
||
| // Get all objects under the common parent path | ||
| List<Map<String, Object>> objects; | ||
| try { | ||
| objects = jsonReadContext.read(commonParentPath); | ||
| } catch (Exception e) { |
There was a problem hiding this comment.
The current implementation is a bit complicated.
In fact, the jsonpath can do this by itself. In jsonpath,
DEFAULT_PATH_LEAF_TO_NULL,
according the comment, when some path leaf not existed, it should return null. That's what we want, so we should check why this configure not work first.
There was a problem hiding this comment.
The current implementation is a bit complicated. In fact, the jsonpath can do this by itself. In jsonpath,
we already enabled
DEFAULT_PATH_LEAF_TO_NULL,
according the comment, when some path leaf not existed, it should return null. That's what we want, so we should check why this configure not work first.
DEFAULT_PATH_LEAF_TO_NULL can't be handled for complex, this configuration is only effective for leaf nodes, and can't be handled for missing parent nodes, see details:
Get parent node according to user configuration, e.g.
key1_1 = "$.result.rows[].key1.key1_1"
key2_1 = "$.result.rows[].key2.key2_1"
will get result.rows[*] as the parent node, and the child node is missing to fill null
There was a problem hiding this comment.
Copilot reviewed 7 out of 8 changed files in this pull request and generated 1 comment.
Files not reviewed (1)
- seatunnel-connectors-v2/connector-http/connector-http-base/pom.xml: Language not supported
Comments suppressed due to low confidence (1)
seatunnel-connectors-v2/connector-http/connector-http-base/src/test/java/org/apache/seatunnel/connectors/seatunnel/http/JsonFieldMissedReturnNullComplexTest.java:182
- The test comment indicates collector.collect is expected to be called once for each JSON object, but the verification uses times(1). Please review the intended behavior and update the comment or verification to avoid confusion.
verify(collector, times(1)).collect(rowCaptor.capture());
| sourceReader.pollNext(collector); | ||
|
|
||
| // Verify collector.collect was called 3 times (once for each JSON object) | ||
| verify(collector, times(1)).collect(rowCaptor.capture()); |
There was a problem hiding this comment.
The comment above this verification states that collector.collect should be called 3 times (once for each JSON object) while the code verifies a single call. Please clarify whether the collector should be invoked once or multiple times and adjust the comment or verification accordingly.
| | pageing.total_page_size | Int | No | - | This parameter is used to control the total number of pages | | ||
| | pageing.batch_size | Int | No | - | The batch size returned per request is used to determine whether to continue when the total number of pages is unknown | | ||
| | pageing.start_page_number | Int | No | 1 | Specify the page number from which synchronization starts | | ||
| | pageing.page_type | String | No | PageNumber | this parameter is used to specify the page type ,or PageNumber if not set, only support `PageNumber` and `Cursor`. | |
There was a problem hiding this comment.
Please rebase on dev. You lose some change from #9109
| - See this link for task configuration [http_jsonpath_to_assert.conf](../../../../seatunnel-e2e/seatunnel-connector-v2-e2e/connector-http-e2e/src/test/resources/http_jsonpath_to_assert.conf). | ||
|
|
||
| ### pageing | ||
| The current supported pagination type are `PageNumber` and `Cursor`. |
| static { | ||
| // Register all available processors in order of precedence | ||
| PROCESSORS.add(new ArrayJsonPathProcessor()); | ||
| PROCESSORS.add(new BracketJsonPathProcessor()); | ||
| PROCESSORS.add(new ObjectJsonPathProcessor()); | ||
| } |
There was a problem hiding this comment.
If a jsonpath contains three features of $.store['book'][*].title, can it still be processed normally? Please add corresponding test cases
| * @param allowNullFields Whether to allow missing fields | ||
| * @return List of data extracted from JSON | ||
| */ | ||
| public static List<Map<String, String>> extractData( |
| @@ -467,8 +447,8 @@ private String getPartOfJson(String data) { | |||
| } | |||
|
|
|||
| private List<List<String>> dataFlip(List<List<String>> results) { | |||
| public static String extractCommonParentPath(JsonPath[] paths) { | ||
| return JsonPathProcessorFactory.getProcessor(paths).extractCommonParentPath(paths); | ||
| } | ||
|
|
||
| /** | ||
| * Gets a relative path from a parent path and a full path. | ||
| * | ||
| * @param parentPath The parent path | ||
| * @param fullPath The full path | ||
| * @return The relative path | ||
| */ | ||
| public static String getRelativePath(String parentPath, String fullPath) { | ||
| return JsonPathProcessorFactory.getProcessor(fullPath) | ||
| .getRelativePath(parentPath, fullPath); | ||
| } |
| List<?> list = (List<?>) value; | ||
| return !list.isEmpty() ? list.get(0).toString() : null; |
There was a problem hiding this comment.
I think return list string instead of return first element is a better choice.
|
|
||
| /** {@inheritDoc} */ | ||
| @Override | ||
| public String extractCommonParentPath(JsonPath[] paths) { |
There was a problem hiding this comment.
But it only used in ArrayJsonPathProcessor. Same as getRelativePath, readObjectsFromPath.
| Field httpClientField = HttpSourceReader.class.getDeclaredField("httpClient"); | ||
| httpClientField.setAccessible(true); | ||
| httpClientField.set(sourceReader, httpClientProvider); |
There was a problem hiding this comment.
Let's use our reflection tool
There was a problem hiding this comment.
Eliminates the use of reflection
| } | ||
| if (value instanceof List) { | ||
| List<?> list = (List<?>) value; | ||
| return !list.isEmpty() ? list.toString() : null; |
There was a problem hiding this comment.
We should return json style list string. Not use list toString value.
There was a problem hiding this comment.
+1, use JsonString replace String
| public class BracketJsonPathProcessor extends AbstractJsonPathProcessor { | ||
| /** {@inheritDoc} */ | ||
| @Override | ||
| public List<List<String>> processJsonData(ReadContext jsonReadContext, JsonPath[] paths) { |
There was a problem hiding this comment.
Recheck the logic of BracketJsonPathProcessor and ObjectJsonPathProcessor. They are totally same. We can keep only one.
There was a problem hiding this comment.
Yes, it is indeed redundant, and I have optimized the relevant code and implementation
| * @param paths Array of JsonPath objects | ||
| * @return The appropriate JsonPathProcessor | ||
| */ | ||
| public static JsonPathProcessor getProcessor(JsonPath[] paths) { |
| @@ -44,37 +44,38 @@ They can be downloaded via install-plugin.sh or from the Maven central repositor | |||
|
|
|||
| ## Source Options | |||
|
The jsonpath util class might be extracted for use by kafka and the file connector |


…is inconsistent with the import failure
Purpose of this pull request
Does this PR introduce any user-facing change?
How was this patch tested?
Check list
New License Guide
release-note.