List<ProjectedTuple> processResult(Json.Result result) {

    final List<ProjectedTuple> tuples = new ArrayList<>();
    if (result.aggregations() != null) {
      Converter<String, Expression> converter = naming.asConverter().reverse();
      // collect values
      Json.visitValueNodes(result.aggregations(), m -> {
        Map<Expression, Object> values = Maps.newHashMapWithExpectedSize(query.projections().size());

        for (String field: m.keySet()) {
          Expression expression = converter.convert(field);
          Object value = m.get(field);
          if (value == null) {
            // otherwise jackson returns null even for optionals
            value = NullNode.getInstance();
          } else if (value instanceof Number && (expression.returnType() == LocalDate.class || expression.returnType() == LocalDateTime.class)) {
            // hack/work-around because JavaTimeModule doesn't handle epoch millis for LocalDate and LocalDateTime
            // and elastic always returns epoch millis
            // this ideally should be handled directly by Deserializer
            Instant instant = Instant.ofEpochMilli(((Number) value).longValue());
            value = nodeFactory.textNode(instant.toString());
          }

          values.put(expression, mapper.convertValue(value, mapper.getTypeFactory().constructType(expression.returnType())));
        }

        List<Object> projections = query.projections().stream().map(values::get).collect(Collectors.toList());
        tuples.add(ProjectedTuple.of(query.projections(), projections));
      });
    }

    // elastic exposes total number of documents matching a query in "/hits/total" path
    // this can be used for simple "select count(*) from table"
    final long total = result.searchHits().total().value();

    return tuples;
  }
Used for constructing (possibly nested) Elastic aggregation nodes.
criteria/elasticsearch/src/org/immutables/criteria/elasticsearch/AggregateQueryBuilder.java