Skip to content

Commit 64f8354

Browse files
Add recursive multi-level dot-notation field filtering
Extends ?fields= to support arbitrary depth: ?fields=data.calcs.ticker walks into "data" (Map), then "calcs" (List of Maps), then filters each element to keep only "ticker" — removing 126 of 127 fields per element without any service-side changes. The key change: writeFilteredNested now re-parses its sub-fields for dots at each level and recurses. A new writeFilteredMap method handles Map<String, Object> containers with recursive descent — the pattern used by JSON-RPC services that return parsed JSON as nested Maps. This enables 97%+ payload reduction on services returning large nested Map/Collection structures (e.g., 5.4MB response with 127 fields per record filtered to ~150KB keeping 5 fields). New unit tests (3 tests modeling Map-of-List-of-Map pattern): - testTwoLevelDotNotationFiltersMapsRecursively - testTwoLevelDotNotationMultipleSubFields - testTwoLevelDotNotation97PercentReduction The Axis2/C implementation supports single-level dot-notation only. Multi-level is an Axis2/Java extension documented in the Javadoc. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent a94e1c5 commit 64f8354

3 files changed

Lines changed: 375 additions & 94 deletions

File tree

modules/json/src/org/apache/axis2/json/streaming/JSONStreamingMessageFormatter.java

Lines changed: 76 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -329,63 +329,110 @@ private void writeFilteredObjectGson(JsonWriter jsonWriter, Object retObj,
329329
}
330330

331331
/**
332-
* Serialize a nested field (Collection, Map, or single POJO) with only
333-
* the specified sub-fields. GSON equivalent of the Moshi
334-
* {@code writeFilteredNested} method.
332+
* Serialize a nested field with recursive dot-notation support (GSON).
333+
* Mirrors the Moshi {@code writeFilteredNested} — parses sub-fields
334+
* for dots at each level and recurses into Maps, Collections, and POJOs.
335335
*/
336336
private void writeFilteredNestedGson(JsonWriter jsonWriter, Object value,
337337
Set<String> subFields, Gson gson,
338338
java.util.Map<Class<?>, List<Field>> fieldCache)
339339
throws IOException {
340340

341+
// Parse sub-fields into immediate keeps and deeper specs
342+
Set<String> immediateKeep = new LinkedHashSet<>();
343+
java.util.Map<String, Set<String>> deeperSpecs = new java.util.LinkedHashMap<>();
344+
for (String spec : subFields) {
345+
int dot = spec.indexOf('.');
346+
if (dot > 0 && dot < spec.length() - 1) {
347+
String container = spec.substring(0, dot);
348+
String remainder = spec.substring(dot + 1);
349+
immediateKeep.add(container);
350+
deeperSpecs.computeIfAbsent(container, k -> new LinkedHashSet<>())
351+
.add(remainder);
352+
} else {
353+
immediateKeep.add(spec);
354+
}
355+
}
356+
341357
if (value instanceof java.util.Collection) {
342358
jsonWriter.beginArray();
343359
for (Object element : (java.util.Collection<?>) value) {
344360
if (element == null) {
345361
jsonWriter.nullValue();
346-
} else {
347-
writeFilteredSingleObjectGson(jsonWriter, element, subFields,
362+
} else if (element instanceof java.util.Map) {
363+
writeFilteredMapGson(jsonWriter, (java.util.Map<?, ?>) element,
364+
immediateKeep, deeperSpecs, gson, fieldCache);
365+
} else if (element instanceof java.util.Collection) {
366+
writeFilteredNestedGson(jsonWriter, element, subFields,
348367
gson, fieldCache);
368+
} else {
369+
writeFilteredPojoGson(jsonWriter, element,
370+
immediateKeep, deeperSpecs, gson, fieldCache);
349371
}
350372
}
351373
jsonWriter.endArray();
352374
} else if (value instanceof java.util.Map) {
353-
jsonWriter.beginObject();
354-
for (java.util.Map.Entry<?, ?> entry : ((java.util.Map<?, ?>) value).entrySet()) {
355-
String key = String.valueOf(entry.getKey());
356-
if (subFields.contains(key)) {
357-
jsonWriter.name(key);
358-
gson.toJson(entry.getValue(), Object.class, jsonWriter);
359-
}
360-
}
361-
jsonWriter.endObject();
375+
writeFilteredMapGson(jsonWriter, (java.util.Map<?, ?>) value,
376+
immediateKeep, deeperSpecs, gson, fieldCache);
362377
} else if (value.getClass().getName().startsWith("java.lang.")) {
363378
gson.toJson(value, value.getClass(), jsonWriter);
364379
} else {
365-
writeFilteredSingleObjectGson(jsonWriter, value, subFields, gson, fieldCache);
380+
writeFilteredPojoGson(jsonWriter, value,
381+
immediateKeep, deeperSpecs, gson, fieldCache);
366382
}
367383
}
368384

369385
/**
370-
* Serialize a single object with only the specified fields using GSON.
371-
* Inner loop of nested filtering — called once per collection element.
386+
* Serialize a Map with recursive field filtering (GSON).
387+
* Mirrors the Moshi {@code writeFilteredMap}.
372388
*/
373-
private void writeFilteredSingleObjectGson(JsonWriter jsonWriter, Object obj,
374-
Set<String> allowedFields, Gson gson,
375-
java.util.Map<Class<?>, List<Field>> fieldCache)
389+
private void writeFilteredMapGson(JsonWriter jsonWriter, java.util.Map<?, ?> map,
390+
Set<String> immediateKeep,
391+
java.util.Map<String, Set<String>> deeperSpecs,
392+
Gson gson,
393+
java.util.Map<Class<?>, List<Field>> fieldCache)
394+
throws IOException {
395+
396+
jsonWriter.beginObject();
397+
for (java.util.Map.Entry<?, ?> entry : map.entrySet()) {
398+
String key = String.valueOf(entry.getKey());
399+
if (!immediateKeep.contains(key)) continue;
400+
401+
jsonWriter.name(key);
402+
Object entryValue = entry.getValue();
403+
Set<String> deeper = deeperSpecs.get(key);
404+
if (deeper != null && entryValue != null) {
405+
writeFilteredNestedGson(jsonWriter, entryValue, deeper, gson, fieldCache);
406+
} else if (entryValue == null) {
407+
jsonWriter.nullValue();
408+
} else {
409+
gson.toJson(entryValue, Object.class, jsonWriter);
410+
}
411+
}
412+
jsonWriter.endObject();
413+
}
414+
415+
/**
416+
* Serialize a POJO with recursive field filtering (GSON).
417+
* Mirrors the Moshi {@code writeFilteredPojo}.
418+
*/
419+
private void writeFilteredPojoGson(JsonWriter jsonWriter, Object pojo,
420+
Set<String> immediateKeep,
421+
java.util.Map<String, Set<String>> deeperSpecs,
422+
Gson gson,
423+
java.util.Map<Class<?>, List<Field>> fieldCache)
376424
throws IOException {
377425

378426
List<Field> fields = fieldCache.computeIfAbsent(
379-
obj.getClass(), JSONStreamingMessageFormatter::getAllFields);
427+
pojo.getClass(), JSONStreamingMessageFormatter::getAllFields);
380428
jsonWriter.beginObject();
381429
for (Field field : fields) {
382-
if (!allowedFields.contains(field.getName())) {
383-
continue;
384-
}
430+
if (!immediateKeep.contains(field.getName())) continue;
431+
385432
Object value;
386433
try {
387434
field.setAccessible(true);
388-
value = field.get(obj);
435+
value = field.get(pojo);
389436
} catch (IllegalAccessException | SecurityException e) {
390437
log.warn("Cannot access field "
391438
+ field.getDeclaringClass().getName().replaceAll("[\r\n]", "_")
@@ -394,7 +441,10 @@ private void writeFilteredSingleObjectGson(JsonWriter jsonWriter, Object obj,
394441
continue;
395442
}
396443
jsonWriter.name(field.getName());
397-
if (value == null) {
444+
Set<String> deeper = deeperSpecs != null ? deeperSpecs.get(field.getName()) : null;
445+
if (deeper != null && value != null) {
446+
writeFilteredNestedGson(jsonWriter, value, deeper, gson, fieldCache);
447+
} else if (value == null) {
398448
jsonWriter.nullValue();
399449
} else {
400450
gson.toJson(value, field.getGenericType(), jsonWriter);

0 commit comments

Comments
 (0)