Reactive Java Mono.zip with Mono.empty() parameters
Asked Answered
L

2

5

Using Spring with Reactor Project to zip multiple api calls as aggregated result. Can Mono.zip() with Mono.empty() parameters return null result?

Mono<Dog> dogMono = dogApiClient.getDog(); // can return Mono.empty()
Mono<Cat> catMono = catMono = catApiClient.getCat(); // can returnMono.empty()
Mono<Horse> horseMono = horseApiClient.getHorse(); // can return Mono.empty()

Mono.zip(dogMono, dogMobo, horseMono)
  .map(this::mapToAnimals);

Expected result:

{
  dog: null, // if dog is null
  cat: null, // if cat is null
  horse: null, // if horse is null
}

Actual result:

{
  dog: {
    name: null,
    surname: null
  }, 
  cat: {
    name: null,
    surname: null
  }, 
  horse: {
    name: null,
    surname: null
  }
}

or

"" // empty
Languid answered 5/8, 2019 at 17:12 Comment(0)
S
5

Well, it's certainly not an elegant solution, but you can choose to wrap your values inside Optional:

Mono<Optional<Dog>> dogMono = Mono.just(Optional.empty());
if(condition1) {
    dogMono = dogApiClient.getDog().map(Optional::of);
}

Mono<Optional<Cat>> catMono = Mono.just(Optional.empty());
if(condition2) {
    catMono = catApiClient.getCat().map(Optional::of);
}

Mono<Optional<Horse>> horseMono = Mono.just(Optional.empty());
if(condition3) {
    horseMono = horseApiClient.getHorse().map(Optional::of);
}

Mono.zip(dogMono, catMono, horseMono)
    .map(this::mapToAnimals);

private Output mapToAnimals(Tuple3<Optional<Dog>, Optional<Cat>, Optional<Horse>> tuple3)
{
    Dog dog = tuple3.getT1().orElse(null);
    Cat cat = tuple3.getT2().orElse(null);
    Horse horse = tuple3.getT3().orElse(null);

    return new Output(dog, cat, horse);
}
Scut answered 5/8, 2019 at 19:3 Comment(0)
S
3

In Reactive Streams, the null value is forbidden. Furthermore, zip expect that all combined publishers have the same number of elements. Or to put it differently: it short-circuits as soon as one of the publishers completes.

So if you use Mono.empty(), that Mono completes immediately and triggers the zip to complete empty as well.

One possible solution would be to have a "null object" instance of each animal, like this:

public static final Dog NO_DOG = new Dog(...);
public static final Cat NO_CAT = new Cat(...);
public static final Horse NO_HORSE = new Horse(...);

Mono<Dog> dogMono = (condition1) ? Mono.just(dogApliClient.getDog()) : Mono.just(NO_DOG);
Mono<Cat> catMono = (condition2) ? Mono.just(catApliClient.getCat()) : Mono.just(NO_CAT);
Mono<Horse> horseMono = (condition3) ? Mono.just(horseApliClient.getHorse()) : Mono.just(NO_HORSE);

Mono.zip(dogMono, catMono, horseMono)
    .map(Animals::fromDogCatAndHorse);

Map<String, Object> fromDogCatAndHorse(Tuple3<Dog, Cat, Horse> tuple) {
    Map<String, Object> forJson = new HashMap<>(3);

    Dog dog = tuple.getT1();
    if (dog = NO_DOG) json.put("dog", null); else json.put("dog", dog);

    Cat cat = tuple.getT2();
    if (cat = NO_CAT) json.put("cat", null); else json.put("cat", cat);

    Horse horse = tuple.getT3();
    if (horse = NO_HORSE) json.put("horse", null); else json.put("horse", horse);

    return forJson;
}

If you can't define these null object instances, then the solution from @yossarian would work too.

Note that there's still a big issue with the api client calls, though: the Mono.just(apiClient.blockingCall()) pattern.

Here you're essentially shoehorning a blocking call inside what is supposed to be a non-blocking controller...

Ideally these clients would return a Mono<Dog|Cat|Horse> to reflect a non-blocking nature. As an example, with a proper non-blocking API, dogMono can be initialized like this:

Mono<Dog> dogMono = (condition1) ? dogApiClient.getDogAsync() : Mono.just(NO_DOG);
Sedillo answered 7/8, 2019 at 13:39 Comment(1)
Thank you for your solutionLanguid

© 2022 - 2024 — McMap. All rights reserved.