From a2e4f3a9632eaa99ad76c97598ee9dafafbf0ed2 Mon Sep 17 00:00:00 2001 From: joseegarcia Date: Tue, 29 Sep 2026 15:57:35 +0200 Subject: [PATCH 1/2] Stream application/jsonl too, and refuse the deprecated application/stream+json JSON Lines is the same format as NDJSON, so it takes the same generated endpoints, and NdjsonSupport now sends the media type the contract declares. ApiWebClient registers it in its codecs and streams it; a WebFlux server or a reactive @HttpExchange interface needs the application's codecs to, which the README shows with a CodecCustomizer. application/stream+json is deprecated and Spring Framework 7 no longer streams it, and ApiWebClient's codecs would not read it: the generation now fails and asks for application/x-ndjson or application/jsonl. Co-Authored-By: Claude Opus 5.5 (1M context) --- README.md | 38 ++++++++- .../plugin/openapi/utils/MapperPathUtil.java | 15 +++- .../resources/templates/openapi/template.ftlh | 2 +- .../openapi/templateNdjsonSupport.ftlh | 14 ++- .../templates/openapi/templateWebClient.ftlh | 16 ++-- .../openapi/OpenApiGeneratorFixtures.java | 6 ++ .../plugin/openapi/OpenApiGeneratorTest.java | 8 ++ .../testNdjsonStreaming/api-stream-json.yml | 23 +++++ .../testNdjsonStreaming/api-test.yml | 27 ++++++ .../assets/httpexchange/ItemsApi.java | 15 ++++ .../assets/imperative-boot3/EventsApi.java | 2 +- .../assets/imperative-boot3/ItemsApi.java | 57 ++++++++++++- .../imperative-boot3/NdjsonSupport.java | 14 ++- .../assets/imperative-boot4/EventsApi.java | 2 +- .../assets/imperative-boot4/ItemsApi.java | 57 ++++++++++++- .../imperative-boot4/NdjsonSupport.java | 14 ++- .../assets/imperative/EventsApi.java | 2 +- .../assets/imperative/ItemsApi.java | 57 ++++++++++++- .../assets/imperative/NdjsonSupport.java | 14 ++- .../assets/reactive/ItemsApi.java | 44 ++++++++++ .../assets/webclient/ItemsApi.java | 85 +++++++++++++++++++ .../springboot4/client/ApiWebClient.java | 10 ++- 22 files changed, 464 insertions(+), 58 deletions(-) create mode 100644 multiapi-engine/src/test/resources/openapigenerator/testNdjsonStreaming/api-stream-json.yml diff --git a/README.md b/README.md index 6837463e..10d94f05 100644 --- a/README.md +++ b/README.md @@ -30,7 +30,7 @@ Maven and Gradle - [Usage](#usage) - [Calling an API from your service (callMode)](#calling-an-api-from-your-service-callmode) - [Object-typed query parameters and multipart bodies](#object-typed-query-parameters-and-multipart-bodies) - - [Streaming endpoints (application/x-ndjson)](#streaming-endpoints-applicationx-ndjson) + - [Streaming endpoints (NDJSON and JSON Lines)](#streaming-endpoints-ndjson-and-json-lines) - [Camel case Java names (useCamelCaseNames)](#camel-case-java-names-usecamelcasenames) - [Unknown enum values (useUnknownEnumValue)](#unknown-enum-values-useunknownenumvalue) - [Property Validation](#property-validation) @@ -1093,12 +1093,14 @@ own: every generated API takes one argument per part (`MultipartFile file, String comment`). A multipart body that references a component schema keeps taking that model. -### Streaming endpoints (application/x-ndjson) +### Streaming endpoints (NDJSON and JSON Lines) An operation whose successful response, or whose request body, is -`application/x-ndjson` (or the older `application/stream+json`) streams one JSON +`application/x-ndjson` or `application/jsonl` (the same format) streams one JSON document per line. Its schema is the type of each item, or an array of it, and -an OpenAPI 3.2 `itemSchema` is read the same way: +an OpenAPI 3.2 `itemSchema` is read the same way. The deprecated +`application/stream+json`, which Spring Framework 7 no longer streams, fails the +generation: ```yaml responses: @@ -1148,6 +1150,34 @@ for it. or a `Mono>>` from `...WithHttpInfo`, and send a streamed body from a `Flux`. **Reactive `@HttpExchange` interfaces** return a `Mono>>`. +- Spring's codecs stream `application/x-ndjson` only. The generated + `ApiWebClient` registers `application/jsonl` too, but a WebFlux server or a + reactive `@HttpExchange` interface uses the application's codecs, which need + it registered for JSON Lines: + + ```java + @Bean + CodecCustomizer jsonLinesCodecCustomizer(final JsonMapper mapper) { + final MediaType jsonLines = new MediaType("application", "jsonl"); + return configurer -> { + final JacksonJsonEncoder encoder = new JacksonJsonEncoder(mapper, + MediaType.APPLICATION_JSON, MediaType.APPLICATION_NDJSON, jsonLines); + encoder.setStreamingMediaTypes( + List.of(MediaType.APPLICATION_NDJSON, jsonLines)); + configurer.defaultCodecs().jacksonJsonEncoder(encoder); + configurer.defaultCodecs().jacksonJsonDecoder(new JacksonJsonDecoder( + mapper, MediaType.APPLICATION_JSON, MediaType.APPLICATION_NDJSON, + jsonLines)); + }; + } + ``` + + This is Spring Boot 4 (`org.springframework.boot.http.codec.CodecCustomizer`). + On Spring Boot 2 and 3 it is + `org.springframework.boot.web.codec.CodecCustomizer`, with an `ObjectMapper` + and `Jackson2JsonEncoder`/`Jackson2JsonDecoder` set through + `jackson2JsonEncoder`/`jackson2JsonDecoder`. The customizer applies to the + WebFlux server and to the WebClients built from Boot's `WebClient.Builder`. - The **blocking clients** (RestClient, and `@HttpExchange` without `reactive`) read a response whole, so they cannot hand the items over as they arrive, and the generation fails for a streaming operation: generate that client with diff --git a/multiapi-engine/src/main/java/com/sngular/api/generator/plugin/openapi/utils/MapperPathUtil.java b/multiapi-engine/src/main/java/com/sngular/api/generator/plugin/openapi/utils/MapperPathUtil.java index 3a56b311..b87aa4a4 100644 --- a/multiapi-engine/src/main/java/com/sngular/api/generator/plugin/openapi/utils/MapperPathUtil.java +++ b/multiapi-engine/src/main/java/com/sngular/api/generator/plugin/openapi/utils/MapperPathUtil.java @@ -61,10 +61,13 @@ public class MapperPathUtil { public static final String SCHEMA = "schema"; /** - * The media types that stream a sequence of JSON documents, one per line, which Spring reads and writes item by item. Their - * schema is the type of each item, or an array of it. + * The media types that stream a sequence of JSON documents, one per line: NDJSON and JSON Lines, which are the same format. + * Their schema is the type of each item, or an array of it. */ - private static final Set STREAMING_MEDIA_TYPES = Set.of("application/x-ndjson", "application/stream+json"); + private static final Set STREAMING_MEDIA_TYPES = Set.of("application/x-ndjson", "application/jsonl"); + + /** Deprecated for application/x-ndjson, and no longer streamed by Spring Framework 7. */ + private static final String STREAM_JSON_MEDIA_TYPE = "application/stream+json"; private static final String JSON_MEDIA_TYPE = "application/json"; @@ -190,6 +193,10 @@ public static boolean isStreamingMediaType(final String mediaType) { * content. */ private static void applyStreaming(final OperationObject operation, final SpecFile specFile) { + if (operation.getProduces().contains(STREAM_JSON_MEDIA_TYPE) || operation.getConsumes().contains(STREAM_JSON_MEDIA_TYPE)) { + throw new CodeGenerationException("Operation '" + operation.getOperationId() + "' declares " + STREAM_JSON_MEDIA_TYPE + ", which is deprecated and " + + "which Spring Framework 7 no longer streams: declare the stream as application/x-ndjson or application/jsonl"); + } final Optional streamedResponse = operation.getResponseObjects().stream() .filter(response -> StringUtils.startsWith(response.getResponseName(), "2")) .filter(response -> response.getContentObjects().stream().anyMatch(ContentObject::isStreaming)) @@ -209,7 +216,7 @@ private static void applyStreaming(final OperationObject operation, final SpecFi } final boolean streams = operation.isStreamingResponse() || operation.getRequestObjects().stream().anyMatch(RequestObject::isStreaming); if (streams && specFile.isCallMode() && !specFile.isReactive()) { - throw new CodeGenerationException("Operation '" + operation.getOperationId() + "' streams application/x-ndjson, which the blocking clients cannot " + throw new CodeGenerationException("Operation '" + operation.getOperationId() + "' streams its items, which the blocking clients cannot " + "consume: generate its client with reactive=true (a WebClient client or a reactive @HttpExchange interface)"); } } diff --git a/multiapi-engine/src/main/resources/templates/openapi/template.ftlh b/multiapi-engine/src/main/resources/templates/openapi/template.ftlh index 8c45b92e..dc8bef57 100644 --- a/multiapi-engine/src/main/resources/templates/openapi/template.ftlh +++ b/multiapi-engine/src/main/resources/templates/openapi/template.ftlh @@ -74,7 +74,7 @@ produces = {"${operation.streamingMediaType}"}${consumesStream} ) default ResponseEntity ${operation.operationId}Ndjson(${streamingEndpointArgs(operation, path, true, true)})${throwsIo} { - return NdjsonSupport.stream(${streamingMethodCall(operation, path)}, servletRequest, servletResponse); + return NdjsonSupport.stream(${streamingMethodCall(operation, path)}, "${operation.streamingMediaType}", servletRequest, servletResponse); } <#if operation.streamingAlsoJson> diff --git a/multiapi-engine/src/main/resources/templates/openapi/templateNdjsonSupport.ftlh b/multiapi-engine/src/main/resources/templates/openapi/templateNdjsonSupport.ftlh index 7c7a6419..1098f27a 100644 --- a/multiapi-engine/src/main/resources/templates/openapi/templateNdjsonSupport.ftlh +++ b/multiapi-engine/src/main/resources/templates/openapi/templateNdjsonSupport.ftlh @@ -25,31 +25,29 @@ import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBo import org.springframework.web.servlet.support.RequestContextUtils; /** - * Sends and reads application/x-ndjson, one JSON document per line, for the API interfaces of this package. Streamed + * Sends and reads application/x-ndjson and application/jsonl, one JSON document per line, for the API interfaces of this package. Streamed * responses are written by Spring MVC on its asynchronous executor, each item sent to the client as soon as it is produced, * and the stream the operation returns is closed once it is sent, also when sending fails. Items are serialized with the * application's ObjectMapper. */ public final class NdjsonSupport { - private static final MediaType APPLICATION_NDJSON = MediaType.parseMediaType("application/x-ndjson"); - private NdjsonSupport() { } /** - * The response streaming the items of the given one, with its status and headers. A response without items, such as the - * NOT_IMPLEMENTED of an operation that is not implemented, is sent without a body. + * The response streaming the items of the given one as the given media type, with its status and headers. A response + * without items, such as the NOT_IMPLEMENTED of an operation that is not implemented, is sent without a body. */ - public static ResponseEntity stream(final ResponseEntity> response, final HttpServletRequest servletRequest, - final HttpServletResponse servletResponse) { + public static ResponseEntity stream(final ResponseEntity> response, final String mediaType, + final HttpServletRequest servletRequest, final HttpServletResponse servletResponse) { final ResponseEntity.BodyBuilder builder = ResponseEntity.status(response.getStatusCode()).headers(response.getHeaders()); final Stream items = response.getBody(); if (items == null) { return builder.build(); } final ObjectMapper mapper = objectMapper(servletRequest); - return builder.contentType(APPLICATION_NDJSON).body(output -> write(items, mapper, output, servletResponse)); + return builder.contentType(MediaType.parseMediaType(mediaType)).body(output -> write(items, mapper, output, servletResponse)); } /** The response sending the items of the given one as a JSON array, with its status and headers. */ diff --git a/multiapi-engine/src/main/resources/templates/openapi/templateWebClient.ftlh b/multiapi-engine/src/main/resources/templates/openapi/templateWebClient.ftlh index 99ca6f53..1459f98e 100644 --- a/multiapi-engine/src/main/resources/templates/openapi/templateWebClient.ftlh +++ b/multiapi-engine/src/main/resources/templates/openapi/templateWebClient.ftlh @@ -6,6 +6,7 @@ import java.text.ParsePosition; import java.time.OffsetDateTime; import java.time.format.DateTimeFormatter; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.Date; @@ -158,17 +159,22 @@ public class ApiWebClient { } private static WebClient buildWebClient(final ${mapperType} mapper) { - // application/x-ndjson is read and written with the same mapper, one item per line. + // application/x-ndjson and application/jsonl are read and written with the same mapper, one item per line. final MediaType ndjson = new MediaType("application", "x-ndjson"); + final MediaType jsonLines = new MediaType("application", "jsonl"); ExchangeStrategies strategies = ExchangeStrategies .builder() .codecs(clientDefaultCodecsConfigurer -> { <#if isJackson3> - clientDefaultCodecsConfigurer.defaultCodecs().jacksonJsonEncoder(new JacksonJsonEncoder(mapper, MediaType.APPLICATION_JSON, ndjson)); - clientDefaultCodecsConfigurer.defaultCodecs().jacksonJsonDecoder(new JacksonJsonDecoder(mapper, MediaType.APPLICATION_JSON, ndjson)); + final JacksonJsonEncoder encoder = new JacksonJsonEncoder(mapper, MediaType.APPLICATION_JSON, ndjson, jsonLines); + encoder.setStreamingMediaTypes(Arrays.asList(ndjson, jsonLines)); + clientDefaultCodecsConfigurer.defaultCodecs().jacksonJsonEncoder(encoder); + clientDefaultCodecsConfigurer.defaultCodecs().jacksonJsonDecoder(new JacksonJsonDecoder(mapper, MediaType.APPLICATION_JSON, ndjson, jsonLines)); <#else> - clientDefaultCodecsConfigurer.defaultCodecs().jackson2JsonEncoder(new Jackson2JsonEncoder(mapper, MediaType.APPLICATION_JSON, ndjson)); - clientDefaultCodecsConfigurer.defaultCodecs().jackson2JsonDecoder(new Jackson2JsonDecoder(mapper, MediaType.APPLICATION_JSON, ndjson)); + final Jackson2JsonEncoder encoder = new Jackson2JsonEncoder(mapper, MediaType.APPLICATION_JSON, ndjson, jsonLines); + encoder.setStreamingMediaTypes(Arrays.asList(ndjson, jsonLines)); + clientDefaultCodecsConfigurer.defaultCodecs().jackson2JsonEncoder(encoder); + clientDefaultCodecsConfigurer.defaultCodecs().jackson2JsonDecoder(new Jackson2JsonDecoder(mapper, MediaType.APPLICATION_JSON, ndjson, jsonLines)); }).build(); WebClient.Builder webClientBuilder = WebClient.builder().exchangeStrategies(strategies); diff --git a/multiapi-engine/src/test/java/com/sngular/api/generator/plugin/openapi/OpenApiGeneratorFixtures.java b/multiapi-engine/src/test/java/com/sngular/api/generator/plugin/openapi/OpenApiGeneratorFixtures.java index 9e78675a..bb737ab8 100644 --- a/multiapi-engine/src/test/java/com/sngular/api/generator/plugin/openapi/OpenApiGeneratorFixtures.java +++ b/multiapi-engine/src/test/java/com/sngular/api/generator/plugin/openapi/OpenApiGeneratorFixtures.java @@ -367,6 +367,12 @@ public final class OpenApiGeneratorFixtures { .modelPackage("com.sngular.multifileplugin.ndjsonmixedrequest.model") .modelNameSuffix("DTO").useTagsGroup(true).build()); + static final List TEST_NDJSON_STREAMING_STREAM_JSON = List + .of(SpecFile.builder().filePath("openapigenerator/testNdjsonStreaming/api-stream-json.yml") + .apiPackage("com.sngular.multifileplugin.ndjsonstreamjson") + .modelPackage("com.sngular.multifileplugin.ndjsonstreamjson.model") + .modelNameSuffix("DTO").useTagsGroup(true).build()); + static final List TEST_HTTP_EXCHANGE_CLIENT = List .of(SpecFile.builder().filePath("openapigenerator/testHttpExchangeClient/api-test.yml") .apiPackage("com.sngular.multifileplugin.httpexchange") diff --git a/multiapi-engine/src/test/java/com/sngular/api/generator/plugin/openapi/OpenApiGeneratorTest.java b/multiapi-engine/src/test/java/com/sngular/api/generator/plugin/openapi/OpenApiGeneratorTest.java index 1c973178..51a6f114 100644 --- a/multiapi-engine/src/test/java/com/sngular/api/generator/plugin/openapi/OpenApiGeneratorTest.java +++ b/multiapi-engine/src/test/java/com/sngular/api/generator/plugin/openapi/OpenApiGeneratorTest.java @@ -275,6 +275,14 @@ void testNdjsonStreamingJsonAlternativeMustListTheItems() { .hasMessageContaining("must be the array of them"); } + @Test + void testDeprecatedStreamJsonIsRefused() { + Assertions.assertThatThrownBy(() -> openApiGenerator.processFileSpec(OpenApiGeneratorFixtures.TEST_NDJSON_STREAMING_STREAM_JSON)) + .isInstanceOf(CodeGenerationException.class) + .hasMessageContaining("listItems") + .hasMessageContaining("application/stream+json"); + } + @Test void testNdjsonStreamedBodyMustBeItsOnlyContent() { Assertions.assertThatThrownBy(() -> openApiGenerator.processFileSpec(OpenApiGeneratorFixtures.TEST_NDJSON_STREAMING_MIXED_REQUEST)) diff --git a/multiapi-engine/src/test/resources/openapigenerator/testNdjsonStreaming/api-stream-json.yml b/multiapi-engine/src/test/resources/openapigenerator/testNdjsonStreaming/api-stream-json.yml new file mode 100644 index 00000000..12f970cf --- /dev/null +++ b/multiapi-engine/src/test/resources/openapigenerator/testNdjsonStreaming/api-stream-json.yml @@ -0,0 +1,23 @@ +openapi: "3.0.3" +info: + title: A stream declared as the deprecated application/stream+json + version: 1.0.0 +paths: + /items: + get: + tags: [items] + operationId: listItems + responses: + '200': + description: One item per line + content: + application/stream+json: + schema: + $ref: "#/components/schemas/Item" +components: + schemas: + Item: + type: object + properties: + id: + type: string diff --git a/multiapi-engine/src/test/resources/openapigenerator/testNdjsonStreaming/api-test.yml b/multiapi-engine/src/test/resources/openapigenerator/testNdjsonStreaming/api-test.yml index 2d2f365a..00bdbe73 100644 --- a/multiapi-engine/src/test/resources/openapigenerator/testNdjsonStreaming/api-test.yml +++ b/multiapi-engine/src/test/resources/openapigenerator/testNdjsonStreaming/api-test.yml @@ -26,6 +26,33 @@ paths: application/json: schema: $ref: "#/components/schemas/Error" + /items/lines: + get: + tags: [items] + operationId: listItemLines + responses: + '200': + description: One item per line, as JSON Lines + content: + application/jsonl: + schema: + $ref: "#/components/schemas/Item" + post: + tags: [items] + operationId: importItemLines + requestBody: + required: true + content: + application/jsonl: + schema: + $ref: "#/components/schemas/Item" + responses: + '200': + description: Import summary + content: + application/json: + schema: + $ref: "#/components/schemas/ImportResult" /items/export: get: tags: [items] diff --git a/multiapi-engine/src/test/resources/openapigenerator/testNdjsonStreaming/assets/httpexchange/ItemsApi.java b/multiapi-engine/src/test/resources/openapigenerator/testNdjsonStreaming/assets/httpexchange/ItemsApi.java index d2917d5e..28089a3c 100644 --- a/multiapi-engine/src/test/resources/openapigenerator/testNdjsonStreaming/assets/httpexchange/ItemsApi.java +++ b/multiapi-engine/src/test/resources/openapigenerator/testNdjsonStreaming/assets/httpexchange/ItemsApi.java @@ -42,6 +42,21 @@ public interface ItemsApi { @GetExchange(url = "/items", accept = {"application/x-ndjson"}) Mono>> listItems(@RequestParam(name = "filter", required = false) String filter); + /** + * GET /items/lines + * @return One item per line, as JSON Lines (status code 200); + */ + @GetExchange(url = "/items/lines", accept = {"application/jsonl"}) + Mono>> listItemLines(); + + /** + * POST /items/lines + * @param itemDTO (required) + * @return Import summary (status code 200); + */ + @PostExchange(url = "/items/lines", accept = {"application/json"}, contentType = "application/jsonl") + Mono> importItemLines(@RequestBody(required = true) Flux itemDTO); + /** * GET /items/export * @return The items, as a JSON array or one per line (status code 200); diff --git a/multiapi-engine/src/test/resources/openapigenerator/testNdjsonStreaming/assets/imperative-boot3/EventsApi.java b/multiapi-engine/src/test/resources/openapigenerator/testNdjsonStreaming/assets/imperative-boot3/EventsApi.java index 34e21974..b6f2f947 100644 --- a/multiapi-engine/src/test/resources/openapigenerator/testNdjsonStreaming/assets/imperative-boot3/EventsApi.java +++ b/multiapi-engine/src/test/resources/openapigenerator/testNdjsonStreaming/assets/imperative-boot3/EventsApi.java @@ -40,7 +40,7 @@ public interface EventsApi { produces = {"application/x-ndjson"} ) default ResponseEntity streamEventsNdjson(@Parameter(hidden = true) final HttpServletRequest servletRequest, @Parameter(hidden = true) final HttpServletResponse servletResponse) { - return NdjsonSupport.stream(streamEvents(), servletRequest, servletResponse); + return NdjsonSupport.stream(streamEvents(), "application/x-ndjson", servletRequest, servletResponse); } /** diff --git a/multiapi-engine/src/test/resources/openapigenerator/testNdjsonStreaming/assets/imperative-boot3/ItemsApi.java b/multiapi-engine/src/test/resources/openapigenerator/testNdjsonStreaming/assets/imperative-boot3/ItemsApi.java index eeba0b35..62a144d2 100644 --- a/multiapi-engine/src/test/resources/openapigenerator/testNdjsonStreaming/assets/imperative-boot3/ItemsApi.java +++ b/multiapi-engine/src/test/resources/openapigenerator/testNdjsonStreaming/assets/imperative-boot3/ItemsApi.java @@ -45,7 +45,7 @@ public interface ItemsApi { produces = {"application/x-ndjson"} ) default ResponseEntity listItemsNdjson(@Parameter(name = "filter", required = false, schema = @Schema(description = "")) @RequestParam(name = "filter", required = false) String filter, @Parameter(hidden = true) final HttpServletRequest servletRequest, @Parameter(hidden = true) final HttpServletResponse servletResponse) { - return NdjsonSupport.stream(listItems(filter), servletRequest, servletResponse); + return NdjsonSupport.stream(listItems(filter), "application/x-ndjson", servletRequest, servletResponse); } /** @@ -55,6 +55,59 @@ default ResponseEntity> listItems(final String filter) { return new ResponseEntity<>(HttpStatus.NOT_IMPLEMENTED); } + /** + * GET /items/lines, sending the items {@link #listItemLines} returns as application/jsonl, one JSON document per line. + */ + @Operation( + operationId = "listItemLines", + tags = {"items"}, + responses = { + @ApiResponse(responseCode = "200", description = "One item per line, as JSON Lines", content = @Content(mediaType = "application/jsonl", schema = @Schema(implementation = ItemDTO.class))) + } + ) + @RequestMapping( + method = RequestMethod.GET, + value = "/items/lines", + produces = {"application/jsonl"} + ) + default ResponseEntity listItemLinesNdjson(@Parameter(hidden = true) final HttpServletRequest servletRequest, @Parameter(hidden = true) final HttpServletResponse servletResponse) { + return NdjsonSupport.stream(listItemLines(), "application/jsonl", servletRequest, servletResponse); + } + + /** + * GET /items/lines. Implement this method rather than the endpoint above: the items are sent as they are produced, and the stream is closed once they are sent. + */ + default ResponseEntity> listItemLines() { + return new ResponseEntity<>(HttpStatus.NOT_IMPLEMENTED); + } + + /** + * POST /items/lines, reading the application/jsonl body, one JSON document per line, as the items {@link #importItemLines} receives. + */ + @Operation( + operationId = "importItemLines", + tags = {"items"}, + responses = { + @ApiResponse(responseCode = "200", description = "Import summary", content = @Content(mediaType = "application/json", schema = @Schema(implementation = ImportResultDTO.class))) + } + ) + @RequestMapping( + method = RequestMethod.POST, + value = "/items/lines", + produces = {"application/json"}, + consumes = {"application/jsonl"} + ) + default ResponseEntity importItemLinesNdjson(@Parameter(hidden = true) final HttpServletRequest servletRequest) throws IOException { + return importItemLines(NdjsonSupport.read(servletRequest, new TypeReference() {})); + } + + /** + * POST /items/lines. Implement this method rather than the endpoint above: the body items are read as the stream is consumed. + */ + default ResponseEntity importItemLines(final Stream itemDTO) { + return new ResponseEntity<>(HttpStatus.NOT_IMPLEMENTED); + } + /** * GET /items/export, sending the items {@link #exportItems} returns as application/x-ndjson, one JSON document per line. */ @@ -71,7 +124,7 @@ default ResponseEntity> listItems(final String filter) { produces = {"application/x-ndjson"} ) default ResponseEntity exportItemsNdjson(@Parameter(hidden = true) final HttpServletRequest servletRequest, @Parameter(hidden = true) final HttpServletResponse servletResponse) { - return NdjsonSupport.stream(exportItems(), servletRequest, servletResponse); + return NdjsonSupport.stream(exportItems(), "application/x-ndjson", servletRequest, servletResponse); } /** diff --git a/multiapi-engine/src/test/resources/openapigenerator/testNdjsonStreaming/assets/imperative-boot3/NdjsonSupport.java b/multiapi-engine/src/test/resources/openapigenerator/testNdjsonStreaming/assets/imperative-boot3/NdjsonSupport.java index 09676305..e734b19e 100644 --- a/multiapi-engine/src/test/resources/openapigenerator/testNdjsonStreaming/assets/imperative-boot3/NdjsonSupport.java +++ b/multiapi-engine/src/test/resources/openapigenerator/testNdjsonStreaming/assets/imperative-boot3/NdjsonSupport.java @@ -21,31 +21,29 @@ import org.springframework.web.servlet.support.RequestContextUtils; /** - * Sends and reads application/x-ndjson, one JSON document per line, for the API interfaces of this package. Streamed + * Sends and reads application/x-ndjson and application/jsonl, one JSON document per line, for the API interfaces of this package. Streamed * responses are written by Spring MVC on its asynchronous executor, each item sent to the client as soon as it is produced, * and the stream the operation returns is closed once it is sent, also when sending fails. Items are serialized with the * application's ObjectMapper. */ public final class NdjsonSupport { - private static final MediaType APPLICATION_NDJSON = MediaType.parseMediaType("application/x-ndjson"); - private NdjsonSupport() { } /** - * The response streaming the items of the given one, with its status and headers. A response without items, such as the - * NOT_IMPLEMENTED of an operation that is not implemented, is sent without a body. + * The response streaming the items of the given one as the given media type, with its status and headers. A response + * without items, such as the NOT_IMPLEMENTED of an operation that is not implemented, is sent without a body. */ - public static ResponseEntity stream(final ResponseEntity> response, final HttpServletRequest servletRequest, - final HttpServletResponse servletResponse) { + public static ResponseEntity stream(final ResponseEntity> response, final String mediaType, + final HttpServletRequest servletRequest, final HttpServletResponse servletResponse) { final ResponseEntity.BodyBuilder builder = ResponseEntity.status(response.getStatusCode()).headers(response.getHeaders()); final Stream items = response.getBody(); if (items == null) { return builder.build(); } final ObjectMapper mapper = objectMapper(servletRequest); - return builder.contentType(APPLICATION_NDJSON).body(output -> write(items, mapper, output, servletResponse)); + return builder.contentType(MediaType.parseMediaType(mediaType)).body(output -> write(items, mapper, output, servletResponse)); } /** The response sending the items of the given one as a JSON array, with its status and headers. */ diff --git a/multiapi-engine/src/test/resources/openapigenerator/testNdjsonStreaming/assets/imperative-boot4/EventsApi.java b/multiapi-engine/src/test/resources/openapigenerator/testNdjsonStreaming/assets/imperative-boot4/EventsApi.java index 34e21974..b6f2f947 100644 --- a/multiapi-engine/src/test/resources/openapigenerator/testNdjsonStreaming/assets/imperative-boot4/EventsApi.java +++ b/multiapi-engine/src/test/resources/openapigenerator/testNdjsonStreaming/assets/imperative-boot4/EventsApi.java @@ -40,7 +40,7 @@ public interface EventsApi { produces = {"application/x-ndjson"} ) default ResponseEntity streamEventsNdjson(@Parameter(hidden = true) final HttpServletRequest servletRequest, @Parameter(hidden = true) final HttpServletResponse servletResponse) { - return NdjsonSupport.stream(streamEvents(), servletRequest, servletResponse); + return NdjsonSupport.stream(streamEvents(), "application/x-ndjson", servletRequest, servletResponse); } /** diff --git a/multiapi-engine/src/test/resources/openapigenerator/testNdjsonStreaming/assets/imperative-boot4/ItemsApi.java b/multiapi-engine/src/test/resources/openapigenerator/testNdjsonStreaming/assets/imperative-boot4/ItemsApi.java index 535bed4b..123979cc 100644 --- a/multiapi-engine/src/test/resources/openapigenerator/testNdjsonStreaming/assets/imperative-boot4/ItemsApi.java +++ b/multiapi-engine/src/test/resources/openapigenerator/testNdjsonStreaming/assets/imperative-boot4/ItemsApi.java @@ -45,7 +45,7 @@ public interface ItemsApi { produces = {"application/x-ndjson"} ) default ResponseEntity listItemsNdjson(@Parameter(name = "filter", required = false, schema = @Schema(description = "")) @RequestParam(name = "filter", required = false) String filter, @Parameter(hidden = true) final HttpServletRequest servletRequest, @Parameter(hidden = true) final HttpServletResponse servletResponse) { - return NdjsonSupport.stream(listItems(filter), servletRequest, servletResponse); + return NdjsonSupport.stream(listItems(filter), "application/x-ndjson", servletRequest, servletResponse); } /** @@ -55,6 +55,59 @@ default ResponseEntity> listItems(final String filter) { return new ResponseEntity<>(HttpStatus.NOT_IMPLEMENTED); } + /** + * GET /items/lines, sending the items {@link #listItemLines} returns as application/jsonl, one JSON document per line. + */ + @Operation( + operationId = "listItemLines", + tags = {"items"}, + responses = { + @ApiResponse(responseCode = "200", description = "One item per line, as JSON Lines", content = @Content(mediaType = "application/jsonl", schema = @Schema(implementation = ItemDTO.class))) + } + ) + @RequestMapping( + method = RequestMethod.GET, + value = "/items/lines", + produces = {"application/jsonl"} + ) + default ResponseEntity listItemLinesNdjson(@Parameter(hidden = true) final HttpServletRequest servletRequest, @Parameter(hidden = true) final HttpServletResponse servletResponse) { + return NdjsonSupport.stream(listItemLines(), "application/jsonl", servletRequest, servletResponse); + } + + /** + * GET /items/lines. Implement this method rather than the endpoint above: the items are sent as they are produced, and the stream is closed once they are sent. + */ + default ResponseEntity> listItemLines() { + return new ResponseEntity<>(HttpStatus.NOT_IMPLEMENTED); + } + + /** + * POST /items/lines, reading the application/jsonl body, one JSON document per line, as the items {@link #importItemLines} receives. + */ + @Operation( + operationId = "importItemLines", + tags = {"items"}, + responses = { + @ApiResponse(responseCode = "200", description = "Import summary", content = @Content(mediaType = "application/json", schema = @Schema(implementation = ImportResultDTO.class))) + } + ) + @RequestMapping( + method = RequestMethod.POST, + value = "/items/lines", + produces = {"application/json"}, + consumes = {"application/jsonl"} + ) + default ResponseEntity importItemLinesNdjson(@Parameter(hidden = true) final HttpServletRequest servletRequest) throws IOException { + return importItemLines(NdjsonSupport.read(servletRequest, new TypeReference() {})); + } + + /** + * POST /items/lines. Implement this method rather than the endpoint above: the body items are read as the stream is consumed. + */ + default ResponseEntity importItemLines(final Stream itemDTO) { + return new ResponseEntity<>(HttpStatus.NOT_IMPLEMENTED); + } + /** * GET /items/export, sending the items {@link #exportItems} returns as application/x-ndjson, one JSON document per line. */ @@ -71,7 +124,7 @@ default ResponseEntity> listItems(final String filter) { produces = {"application/x-ndjson"} ) default ResponseEntity exportItemsNdjson(@Parameter(hidden = true) final HttpServletRequest servletRequest, @Parameter(hidden = true) final HttpServletResponse servletResponse) { - return NdjsonSupport.stream(exportItems(), servletRequest, servletResponse); + return NdjsonSupport.stream(exportItems(), "application/x-ndjson", servletRequest, servletResponse); } /** diff --git a/multiapi-engine/src/test/resources/openapigenerator/testNdjsonStreaming/assets/imperative-boot4/NdjsonSupport.java b/multiapi-engine/src/test/resources/openapigenerator/testNdjsonStreaming/assets/imperative-boot4/NdjsonSupport.java index 0ae8f5d3..5e1cd828 100644 --- a/multiapi-engine/src/test/resources/openapigenerator/testNdjsonStreaming/assets/imperative-boot4/NdjsonSupport.java +++ b/multiapi-engine/src/test/resources/openapigenerator/testNdjsonStreaming/assets/imperative-boot4/NdjsonSupport.java @@ -22,31 +22,29 @@ import org.springframework.web.servlet.support.RequestContextUtils; /** - * Sends and reads application/x-ndjson, one JSON document per line, for the API interfaces of this package. Streamed + * Sends and reads application/x-ndjson and application/jsonl, one JSON document per line, for the API interfaces of this package. Streamed * responses are written by Spring MVC on its asynchronous executor, each item sent to the client as soon as it is produced, * and the stream the operation returns is closed once it is sent, also when sending fails. Items are serialized with the * application's ObjectMapper. */ public final class NdjsonSupport { - private static final MediaType APPLICATION_NDJSON = MediaType.parseMediaType("application/x-ndjson"); - private NdjsonSupport() { } /** - * The response streaming the items of the given one, with its status and headers. A response without items, such as the - * NOT_IMPLEMENTED of an operation that is not implemented, is sent without a body. + * The response streaming the items of the given one as the given media type, with its status and headers. A response + * without items, such as the NOT_IMPLEMENTED of an operation that is not implemented, is sent without a body. */ - public static ResponseEntity stream(final ResponseEntity> response, final HttpServletRequest servletRequest, - final HttpServletResponse servletResponse) { + public static ResponseEntity stream(final ResponseEntity> response, final String mediaType, + final HttpServletRequest servletRequest, final HttpServletResponse servletResponse) { final ResponseEntity.BodyBuilder builder = ResponseEntity.status(response.getStatusCode()).headers(response.getHeaders()); final Stream items = response.getBody(); if (items == null) { return builder.build(); } final ObjectMapper mapper = objectMapper(servletRequest); - return builder.contentType(APPLICATION_NDJSON).body(output -> write(items, mapper, output, servletResponse)); + return builder.contentType(MediaType.parseMediaType(mediaType)).body(output -> write(items, mapper, output, servletResponse)); } /** The response sending the items of the given one as a JSON array, with its status and headers. */ diff --git a/multiapi-engine/src/test/resources/openapigenerator/testNdjsonStreaming/assets/imperative/EventsApi.java b/multiapi-engine/src/test/resources/openapigenerator/testNdjsonStreaming/assets/imperative/EventsApi.java index afb483d7..1aa2b340 100644 --- a/multiapi-engine/src/test/resources/openapigenerator/testNdjsonStreaming/assets/imperative/EventsApi.java +++ b/multiapi-engine/src/test/resources/openapigenerator/testNdjsonStreaming/assets/imperative/EventsApi.java @@ -40,7 +40,7 @@ public interface EventsApi { produces = {"application/x-ndjson"} ) default ResponseEntity streamEventsNdjson(@Parameter(hidden = true) final HttpServletRequest servletRequest, @Parameter(hidden = true) final HttpServletResponse servletResponse) { - return NdjsonSupport.stream(streamEvents(), servletRequest, servletResponse); + return NdjsonSupport.stream(streamEvents(), "application/x-ndjson", servletRequest, servletResponse); } /** diff --git a/multiapi-engine/src/test/resources/openapigenerator/testNdjsonStreaming/assets/imperative/ItemsApi.java b/multiapi-engine/src/test/resources/openapigenerator/testNdjsonStreaming/assets/imperative/ItemsApi.java index 9b9402b4..614352fe 100644 --- a/multiapi-engine/src/test/resources/openapigenerator/testNdjsonStreaming/assets/imperative/ItemsApi.java +++ b/multiapi-engine/src/test/resources/openapigenerator/testNdjsonStreaming/assets/imperative/ItemsApi.java @@ -45,7 +45,7 @@ public interface ItemsApi { produces = {"application/x-ndjson"} ) default ResponseEntity listItemsNdjson(@Parameter(name = "filter", required = false, schema = @Schema(description = "")) @RequestParam(name = "filter", required = false) String filter, @Parameter(hidden = true) final HttpServletRequest servletRequest, @Parameter(hidden = true) final HttpServletResponse servletResponse) { - return NdjsonSupport.stream(listItems(filter), servletRequest, servletResponse); + return NdjsonSupport.stream(listItems(filter), "application/x-ndjson", servletRequest, servletResponse); } /** @@ -55,6 +55,59 @@ default ResponseEntity> listItems(final String filter) { return new ResponseEntity<>(HttpStatus.NOT_IMPLEMENTED); } + /** + * GET /items/lines, sending the items {@link #listItemLines} returns as application/jsonl, one JSON document per line. + */ + @Operation( + operationId = "listItemLines", + tags = {"items"}, + responses = { + @ApiResponse(responseCode = "200", description = "One item per line, as JSON Lines", content = @Content(mediaType = "application/jsonl", schema = @Schema(implementation = ItemDTO.class))) + } + ) + @RequestMapping( + method = RequestMethod.GET, + value = "/items/lines", + produces = {"application/jsonl"} + ) + default ResponseEntity listItemLinesNdjson(@Parameter(hidden = true) final HttpServletRequest servletRequest, @Parameter(hidden = true) final HttpServletResponse servletResponse) { + return NdjsonSupport.stream(listItemLines(), "application/jsonl", servletRequest, servletResponse); + } + + /** + * GET /items/lines. Implement this method rather than the endpoint above: the items are sent as they are produced, and the stream is closed once they are sent. + */ + default ResponseEntity> listItemLines() { + return new ResponseEntity<>(HttpStatus.NOT_IMPLEMENTED); + } + + /** + * POST /items/lines, reading the application/jsonl body, one JSON document per line, as the items {@link #importItemLines} receives. + */ + @Operation( + operationId = "importItemLines", + tags = {"items"}, + responses = { + @ApiResponse(responseCode = "200", description = "Import summary", content = @Content(mediaType = "application/json", schema = @Schema(implementation = ImportResultDTO.class))) + } + ) + @RequestMapping( + method = RequestMethod.POST, + value = "/items/lines", + produces = {"application/json"}, + consumes = {"application/jsonl"} + ) + default ResponseEntity importItemLinesNdjson(@Parameter(hidden = true) final HttpServletRequest servletRequest) throws IOException { + return importItemLines(NdjsonSupport.read(servletRequest, new TypeReference() {})); + } + + /** + * POST /items/lines. Implement this method rather than the endpoint above: the body items are read as the stream is consumed. + */ + default ResponseEntity importItemLines(final Stream itemDTO) { + return new ResponseEntity<>(HttpStatus.NOT_IMPLEMENTED); + } + /** * GET /items/export, sending the items {@link #exportItems} returns as application/x-ndjson, one JSON document per line. */ @@ -71,7 +124,7 @@ default ResponseEntity> listItems(final String filter) { produces = {"application/x-ndjson"} ) default ResponseEntity exportItemsNdjson(@Parameter(hidden = true) final HttpServletRequest servletRequest, @Parameter(hidden = true) final HttpServletResponse servletResponse) { - return NdjsonSupport.stream(exportItems(), servletRequest, servletResponse); + return NdjsonSupport.stream(exportItems(), "application/x-ndjson", servletRequest, servletResponse); } /** diff --git a/multiapi-engine/src/test/resources/openapigenerator/testNdjsonStreaming/assets/imperative/NdjsonSupport.java b/multiapi-engine/src/test/resources/openapigenerator/testNdjsonStreaming/assets/imperative/NdjsonSupport.java index aa8abfe7..1c018a09 100644 --- a/multiapi-engine/src/test/resources/openapigenerator/testNdjsonStreaming/assets/imperative/NdjsonSupport.java +++ b/multiapi-engine/src/test/resources/openapigenerator/testNdjsonStreaming/assets/imperative/NdjsonSupport.java @@ -21,31 +21,29 @@ import org.springframework.web.servlet.support.RequestContextUtils; /** - * Sends and reads application/x-ndjson, one JSON document per line, for the API interfaces of this package. Streamed + * Sends and reads application/x-ndjson and application/jsonl, one JSON document per line, for the API interfaces of this package. Streamed * responses are written by Spring MVC on its asynchronous executor, each item sent to the client as soon as it is produced, * and the stream the operation returns is closed once it is sent, also when sending fails. Items are serialized with the * application's ObjectMapper. */ public final class NdjsonSupport { - private static final MediaType APPLICATION_NDJSON = MediaType.parseMediaType("application/x-ndjson"); - private NdjsonSupport() { } /** - * The response streaming the items of the given one, with its status and headers. A response without items, such as the - * NOT_IMPLEMENTED of an operation that is not implemented, is sent without a body. + * The response streaming the items of the given one as the given media type, with its status and headers. A response + * without items, such as the NOT_IMPLEMENTED of an operation that is not implemented, is sent without a body. */ - public static ResponseEntity stream(final ResponseEntity> response, final HttpServletRequest servletRequest, - final HttpServletResponse servletResponse) { + public static ResponseEntity stream(final ResponseEntity> response, final String mediaType, + final HttpServletRequest servletRequest, final HttpServletResponse servletResponse) { final ResponseEntity.BodyBuilder builder = ResponseEntity.status(response.getStatusCode()).headers(response.getHeaders()); final Stream items = response.getBody(); if (items == null) { return builder.build(); } final ObjectMapper mapper = objectMapper(servletRequest); - return builder.contentType(APPLICATION_NDJSON).body(output -> write(items, mapper, output, servletResponse)); + return builder.contentType(MediaType.parseMediaType(mediaType)).body(output -> write(items, mapper, output, servletResponse)); } /** The response sending the items of the given one as a JSON array, with its status and headers. */ diff --git a/multiapi-engine/src/test/resources/openapigenerator/testNdjsonStreaming/assets/reactive/ItemsApi.java b/multiapi-engine/src/test/resources/openapigenerator/testNdjsonStreaming/assets/reactive/ItemsApi.java index ca926590..040a8d5b 100644 --- a/multiapi-engine/src/test/resources/openapigenerator/testNdjsonStreaming/assets/reactive/ItemsApi.java +++ b/multiapi-engine/src/test/resources/openapigenerator/testNdjsonStreaming/assets/reactive/ItemsApi.java @@ -50,6 +50,50 @@ default ResponseEntity> listItems(@Parameter(name = "filter", requ return new ResponseEntity<>(HttpStatus.NOT_IMPLEMENTED); } + /** + * GET /items/lines + * @return One item per line, as JSON Lines; (status code 200) + * @throws WebClientResponseException if an error occurs while attempting to invoke the API + */ + @Operation( + operationId = "listItemLines", + tags = {"items"}, + responses = { + @ApiResponse(responseCode = "200", description = "One item per line, as JSON Lines", content = @Content(mediaType = "application/jsonl", schema = @Schema(implementation = ItemDTO.class))) + } + ) + @RequestMapping( + method = RequestMethod.GET, + value = "/items/lines", + produces = {"application/jsonl"} + ) + default ResponseEntity> listItemLines(@ApiIgnore final ServerWebExchange exchange) { + return new ResponseEntity<>(HttpStatus.NOT_IMPLEMENTED); + } + + /** + * POST /items/lines + * @param itemDTO (required) + * @return Import summary; (status code 200) + * @throws WebClientResponseException if an error occurs while attempting to invoke the API + */ + @Operation( + operationId = "importItemLines", + tags = {"items"}, + responses = { + @ApiResponse(responseCode = "200", description = "Import summary", content = @Content(mediaType = "application/json", schema = @Schema(implementation = ImportResultDTO.class))) + } + ) + @RequestMapping( + method = RequestMethod.POST, + value = "/items/lines", + produces = {"application/json"}, + consumes = {"application/jsonl"} + ) + default ResponseEntity> importItemLines(@Parameter(name = "itemDTO", description = "", required = true, schema = @Schema(description = "")) @Valid @RequestBody Flux itemDTO, @ApiIgnore final ServerWebExchange exchange) { + return new ResponseEntity<>(HttpStatus.NOT_IMPLEMENTED); + } + /** * GET /items/export * @return The items, as a JSON array or one per line; (status code 200) diff --git a/multiapi-engine/src/test/resources/openapigenerator/testNdjsonStreaming/assets/webclient/ItemsApi.java b/multiapi-engine/src/test/resources/openapigenerator/testNdjsonStreaming/assets/webclient/ItemsApi.java index 227a3810..a315de93 100644 --- a/multiapi-engine/src/test/resources/openapigenerator/testNdjsonStreaming/assets/webclient/ItemsApi.java +++ b/multiapi-engine/src/test/resources/openapigenerator/testNdjsonStreaming/assets/webclient/ItemsApi.java @@ -114,6 +114,91 @@ public Mono>> listItemsWithHttpInfo(String filter) return listItemsRequestCreation(filter).toEntityFlux(localVarReturnType); } + /** + * GET /items/lines: "" + * @return One item per line, as JSON Lines; (status code 200) + * @throws WebClientResponseException if an error occurs while attempting to invoke the API + */ + private ResponseSpec listItemLinesRequestCreation() throws WebClientResponseException { + Object postBody = null; + final Map pathParams = new HashMap(); + + final MultiValueMap queryParams = new LinkedMultiValueMap(); + final HttpHeaders headerParams = new HttpHeaders(); + final MultiValueMap cookieParams = new LinkedMultiValueMap(); + final MultiValueMap formParams = new LinkedMultiValueMap(); + final String[] localVarAccepts = {"application/jsonl"}; + final List localVarAccept = apiWebClient.selectHeaderAccept(localVarAccepts); + final String[] localVarContentTypes = {}; + final MediaType localVarContentType = apiWebClient.selectHeaderContentType(localVarContentTypes); + + String[] localVarAuthNames = new String[] {}; + + ParameterizedTypeReference localVarReturnType = new ParameterizedTypeReference() {}; + return apiWebClient.invokeAPI(basePath,"/items/lines", HttpMethod.GET, pathParams, queryParams, postBody, headerParams, cookieParams, formParams, localVarAccept, localVarContentType, localVarAuthNames, localVarReturnType); + + } + + /** + * GET /items/lines + * @return One item per line, as JSON Lines; (status code 200) + * @throws WebClientResponseException if an error occurs while attempting to invoke the API + */ + public Flux listItemLines() throws WebClientResponseException { + ParameterizedTypeReference localVarReturnType = new ParameterizedTypeReference() {}; + return listItemLinesRequestCreation().bodyToFlux(localVarReturnType); + } + + public Mono>> listItemLinesWithHttpInfo() throws WebClientResponseException { + ParameterizedTypeReference localVarReturnType = new ParameterizedTypeReference() {}; + return listItemLinesRequestCreation().toEntityFlux(localVarReturnType); + } + + /** + * POST /items/lines: "" + * @param itemDTO (required) + * @return Import summary; (status code 200) + * @throws WebClientResponseException if an error occurs while attempting to invoke the API + */ + private ResponseSpec importItemLinesRequestCreation(Flux itemDTO) throws WebClientResponseException { + Object postBody = itemDTO; + if (itemDTO == null) { + throw new WebClientResponseException("Missing the required parameter ''itemDTO'' when calling importItemLines", HttpStatus.BAD_REQUEST.value(), HttpStatus.BAD_REQUEST.getReasonPhrase(), null, null, null); + } + final Map pathParams = new HashMap(); + + final MultiValueMap queryParams = new LinkedMultiValueMap(); + final HttpHeaders headerParams = new HttpHeaders(); + final MultiValueMap cookieParams = new LinkedMultiValueMap(); + final MultiValueMap formParams = new LinkedMultiValueMap(); + final String[] localVarAccepts = {"application/json"}; + final List localVarAccept = apiWebClient.selectHeaderAccept(localVarAccepts); + final String[] localVarContentTypes = {"application/jsonl"}; + final MediaType localVarContentType = apiWebClient.selectHeaderContentType(localVarContentTypes); + + String[] localVarAuthNames = new String[] {}; + + ParameterizedTypeReference localVarReturnType = new ParameterizedTypeReference() {}; + return apiWebClient.invokeAPI(basePath,"/items/lines", HttpMethod.POST, pathParams, queryParams, postBody, headerParams, cookieParams, formParams, localVarAccept, localVarContentType, localVarAuthNames, localVarReturnType); + + } + + /** + * POST /items/lines + * @param itemDTO (required) + * @return Import summary; (status code 200) + * @throws WebClientResponseException if an error occurs while attempting to invoke the API + */ + public Mono importItemLines(Flux itemDTO) throws WebClientResponseException { + ParameterizedTypeReference localVarReturnType = new ParameterizedTypeReference() {}; + return importItemLinesRequestCreation(itemDTO).bodyToMono(localVarReturnType); + } + + public Mono> importItemLinesWithHttpInfo(Flux itemDTO) throws WebClientResponseException { + ParameterizedTypeReference localVarReturnType = new ParameterizedTypeReference() {}; + return importItemLinesRequestCreation(itemDTO).toEntity(localVarReturnType); + } + /** * GET /items/export: "" * @return The items, as a JSON array or one per line; (status code 200) diff --git a/multiapi-engine/src/test/resources/openapigenerator/testWebClientApiGeneration/assets/springboot4/client/ApiWebClient.java b/multiapi-engine/src/test/resources/openapigenerator/testWebClientApiGeneration/assets/springboot4/client/ApiWebClient.java index 69a1b0e8..8f877b07 100644 --- a/multiapi-engine/src/test/resources/openapigenerator/testWebClientApiGeneration/assets/springboot4/client/ApiWebClient.java +++ b/multiapi-engine/src/test/resources/openapigenerator/testWebClientApiGeneration/assets/springboot4/client/ApiWebClient.java @@ -6,6 +6,7 @@ import java.time.OffsetDateTime; import java.time.format.DateTimeFormatter; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.Date; @@ -137,13 +138,16 @@ private static JsonMapper createDefaultObjectMapper(final DateFormat defaultDate } private static WebClient buildWebClient(final JsonMapper mapper) { - // application/x-ndjson is read and written with the same mapper, one item per line. + // application/x-ndjson and application/jsonl are read and written with the same mapper, one item per line. final MediaType ndjson = new MediaType("application", "x-ndjson"); + final MediaType jsonLines = new MediaType("application", "jsonl"); ExchangeStrategies strategies = ExchangeStrategies .builder() .codecs(clientDefaultCodecsConfigurer -> { - clientDefaultCodecsConfigurer.defaultCodecs().jacksonJsonEncoder(new JacksonJsonEncoder(mapper, MediaType.APPLICATION_JSON, ndjson)); - clientDefaultCodecsConfigurer.defaultCodecs().jacksonJsonDecoder(new JacksonJsonDecoder(mapper, MediaType.APPLICATION_JSON, ndjson)); + final JacksonJsonEncoder encoder = new JacksonJsonEncoder(mapper, MediaType.APPLICATION_JSON, ndjson, jsonLines); + encoder.setStreamingMediaTypes(Arrays.asList(ndjson, jsonLines)); + clientDefaultCodecsConfigurer.defaultCodecs().jacksonJsonEncoder(encoder); + clientDefaultCodecsConfigurer.defaultCodecs().jacksonJsonDecoder(new JacksonJsonDecoder(mapper, MediaType.APPLICATION_JSON, ndjson, jsonLines)); }).build(); WebClient.Builder webClientBuilder = WebClient.builder().exchangeStrategies(strategies); return webClientBuilder.build(); From def58f333a5494fec3204781668d1f3820967f7f Mon Sep 17 00:00:00 2001 From: joseegarcia Date: Tue, 29 Sep 2026 16:01:01 +0200 Subject: [PATCH 2/2] Bump version to 8.3.1 Co-Authored-By: Claude Opus 5.5 (1M context) --- multiapi-engine/pom.xml | 2 +- scs-multiapi-gradle-plugin/build.gradle | 4 ++-- scs-multiapi-maven-plugin/pom.xml | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/multiapi-engine/pom.xml b/multiapi-engine/pom.xml index 66440ce9..560692d4 100644 --- a/multiapi-engine/pom.xml +++ b/multiapi-engine/pom.xml @@ -4,7 +4,7 @@ com.sngular multiapi-engine - 8.3.0 + 8.3.1 jar diff --git a/scs-multiapi-gradle-plugin/build.gradle b/scs-multiapi-gradle-plugin/build.gradle index 6ee459d5..d2d73075 100644 --- a/scs-multiapi-gradle-plugin/build.gradle +++ b/scs-multiapi-gradle-plugin/build.gradle @@ -21,7 +21,7 @@ repositories { } group = 'com.sngular' -version = '8.3.0' +version = '8.3.1' def SCSMultiApiPluginGroupId = group def SCSMultiApiPluginVersion = version @@ -31,7 +31,7 @@ dependencies { shadow localGroovy() shadow gradleApi() - implementation 'com.sngular:multiapi-engine:8.3.0' + implementation 'com.sngular:multiapi-engine:8.3.1' testImplementation 'org.assertj:assertj-core:3.24.2' testImplementation 'com.puppycrawl.tools:checkstyle:10.12.3' testImplementation 'org.junit.platform:junit-platform-launcher:1.9.2' diff --git a/scs-multiapi-maven-plugin/pom.xml b/scs-multiapi-maven-plugin/pom.xml index f97b5455..8cb25044 100644 --- a/scs-multiapi-maven-plugin/pom.xml +++ b/scs-multiapi-maven-plugin/pom.xml @@ -4,7 +4,7 @@ com.sngular scs-multiapi-maven-plugin - 8.3.0 + 8.3.1 maven-plugin AsyncApi - OpenApi Code Generator Maven Plugin @@ -272,7 +272,7 @@ com.sngular multiapi-engine - 8.3.0 + 8.3.1 org.apache.maven