Keep parsing JSON response until a field becomes false using GSON
Overall I think it is a good solution.
Exception handling
I think getIds()-method cannot handle exceptions properly in the context of any application.
Currently it makes the result look like as nothing has happened even an exception is thrown. I do not think that callers can make valid assumptions on the returned value so I suggest to encapsulate the general Exception into a RuntimeException (or some of your own). The caller of the method should know if the result is valid. If you want the caller continue to process with partial results then you can make your own exception containing the information received so far. But to abstract from errors is dangerous.
The decision of throwing exceptions is as important as catching, rethrowing, wrapping, logging and handle them.
If you throw an exception you do it because you want to indicate that the assumptions a method has are not met so it cannot fulfill its work.
If you want to rethrow an exception the purpose mostly is to intercept information.
If you wrap an exception you want to

map the Exception to an exception of YOUR domain
make the Exception signature irrelevant (RuntimeException)

An exception should be handled at a point it can be handled. And this is not neccessarily the method it occurs.
Logging an exception is the last option for exceptional cases that have no previous mentioned handle. And this is also not neccessarily the method it occurs.
Remove Optional-construct
To exit the loop you determine the existance of a next &quot;url&quot;. That should be your single point of truth. &quot;getResponse&quot; is called if &quot;hasMore&quot; is true. But If &quot;getResponse&quot; gets called then it is called under the assumption that there &quot;is&quot; more. If the getResponse-method cannot fulfill the work it is an exception. My view is that getting an &quot;Optional&quot; when &quot;hasMore&quot; has determined before is contradictory.
Throw an exception and the same mechanism as described in &quot;Exception handling&quot; is used. It is transparent to the caller. The caller always will get all necessary information and can decide on its own what to do. Either the getIds()-method returns normal or an exception is thrown that contains the Ids received so far.
Ambigious next-key and semantic redundancy
Currently you have two next-keys holding different values: boolean and String. Theoretically a JsonObject should have unique keys. But any behaviour you rely on to retreive values from this key is specific to the JSON library you use. Maybe GSON does it right somehow.
Furthermore you have semantical redundancy. The existance of the next URL is sufficient information to determine if there is a next URL.
My suggestion is to get rid of the semantic redundancy as early as possible so that further algorithms work with a single source of truth. Maybe you have a preprocession of the Json String and return the JsonObject as the return value of your execute-method().
Reuse JsonParser
I think that is a little issue. But I would reuse the JsonParser-instance as I do not know if this is an expensive operation to instantiate this kind of object.
Extracting constants
There are code fragments that should be extracted to constants. the &quot;key&quot; String is repeated but adresses the same semantic. But also the other keys to access the json object.
Variable scope
The subjects become pedantic... You may reduce the scope of the &quot;hasMore&quot; variable by using a for loop rather than a while loop.
Types
You should also have a look at your GSON-Types. To determine the id you ask an JsonElement. A JsonElement may also be a JsonNull-Object (see here). Maybe you have to introduce some type-check.
Code
private static final String KEY_NEXT = &quot;next&quot;;
private static final String KEY_URL = &quot;url&quot;;
private static final String KEY_RESULT = &quot;result&quot;;
private static final String KEY_ID = &quot;id&quot;;


public List&lt;String&gt; getIds() {
    List&lt;String&gt; ids = new ArrayList&lt;&gt;();
    try {
        String url = generateUrl();
        final JsonParser jsonParser = new JsonParser();
        for (boolean hasMore = true; hasMore;) {
            JsonObject jobject = execute(url);
            JsonArray jarray = jobject.getAsJsonArray(KEY_RESULT);
            for (JsonElement type : jarray) {
                ids.add(type.getAsJsonObject().get(KEY_ID).getAsString());
            }
            hasMore = jobject.has(KEY_NEXT); // assumption, that only the next-key with the url is possibly present
            if (hasMore) {
                url = PREFIX + jobject.getAsJsonObject(KEY_NEXT).get(KEY_URL).getAsString();
            }
        }
    } catch (Exception ex) {
        throw new PartialResultException(ex, ids); 
    }
    return ids;
}


private static class PartialResultException extends RuntimeException {

    private List&lt;String&gt; idsReceivedSoFar;

    public PartialResultException(Exception ex, List&lt;String&gt; idsReceivedSoFar) {
        super(ex);
        this.idsReceivedSoFar = idsReceivedSoFar;
    }

    public List&lt;String&gt; getIdsReceivedSoFar() {
        return idsReceivedSoFar;
    }
    
}


// Example call
public void caller() {
    
    List&lt;String&gt; ids = new ArrayList&lt;&gt;();
    
    try {
        ids.addAll(getIds());
    } catch (PartialResultException e) {
        // omit this if the caller is not interested in the ids received so far if an exception occured
        ids.addAll(e.getIdsReceivedSoFar());
    }
    
    // code that uses the ids
    for (String string : ids) {
        ...
    }
    
}

The execute-method will either wrap the occuring exceptions to a RuntimeException...
private JsonObject execute(final String url) {
    try (AsyncHttpClient asyncHttpClient = new DefaultAsyncHttpClient()) {
        Future&lt;Response&gt; future = asyncHttpClient.prepareGet(url).execute();
        Response response = future.get();
        final JsonParser jsonParser = new JsonParser();
        JsobObject jsonResponse = jsonParser.parse(response.getResponseBody(StandardCharsets.UTF_8.displayName())).getAsJsonObject();
        removeNextKeyBooleanValue(jsonParser);
        return jsonResponse;
    } catch (IOException | InterruptedException | ExecutionException ex) {
        throw new RuntimeException(ex);
    }
}      
  

... or you may throw them
private JsonObject execute(final String url) throws IOException, InterruptedException, ExecutionException {
    try (AsyncHttpClient asyncHttpClient = new DefaultAsyncHttpClient()) {
        Future&lt;Response&gt; future = asyncHttpClient.prepareGet(url).execute();
        Response response = future.get();
        final JsonParser jsonParser = new JsonParser();
        JsobObject jsonResponse = jsonParser.parse(response.getResponseBody(StandardCharsets.UTF_8.displayName())).getAsJsonObject();
        removeNextKeyBooleanValue(jsonParser);
        return jsonResponse;
    }
}

The point is that there is no rule to catch and handle an exception where it occurs or as early as possible. The rule is: it has to be the semantic correct location under the assumptions of the involved methods.