DTO from JSON with dynamic key

依然范特西╮ 提交于 2020-01-11 07:25:42

问题


I'm trying to figure out how to write a nice DTO for a Spring Boot app that's proxying search capabilities to another (Python) service.

So I currently have an almost perfect setup going. I'm only having problems with representing the aggregations I get back from Elasticsearch as objects on the Java side.

Here's the current Aggregation DTO:

package com.example.dto.search;

import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;

import java.util.List;
import java.util.Map;

@Getter @Setter @NoArgsConstructor
public class Aggregation {
    private List<Map<String, Object>> buckets;
    private int docCountErrorUpperBound;
    private int sumOtherDocCount;
}

Looking at the JSON representation though, which looks like this:

{
  "aggregations": {
    "categories": {
      "buckets": [
        {
          "doc_count": 12,
          "key": "IT",
          "sub_categories": {
            "buckets": [
              {
                "doc_count": 12,
                "key": "Programming"
              }
            ],
            "doc_count_error_upper_bound": 0,
            "sum_other_doc_count": 0
          }
        },
        {
          "doc_count": 1,
          "key": "Handy Man",
          "sub_categories": {
            "buckets": [
              {
                "doc_count": 1,
                "key": "Plumbing"
              }
            ],
            "doc_count_error_upper_bound": 0,
            "sum_other_doc_count": 0
          }
        }
      ],
      "docCountErrorUpperBound": 0,
      "sumOtherDocCount": 0
    },
....

I'm pretty sure that I can change the buckets property like so:

package com.example.dto.search;

import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;

import java.util.List;
import java.util.Map;

@Getter @Setter @NoArgsConstructor
public class Aggregation {
    private List<Bucket> buckets;
    private int docCountErrorUpperBound;
    private int sumOtherDocCount;
}

with a bucket class starting out like this

package com.example.dto.search;

public class Bucket {
    private int docCount;
    private String key;
    //What do I do here for sub_categories???
}

But as you can see from the JSON, the sub_categories key is the problem, as it is a dynamic name. It will also be of type Bucket, since the buckets can be nested in Elasticsearch.

Any ideas on how to represent these buckets as custom objects and not just a Map?


回答1:


You can use custom serializer for building dynamic JSON responses. But you should somehow pass you dynamic category name to this serializer.

In my example I store it as Entity member - private String instance. (If you using JPA Entities, use @Transient annotation for not mapping this field to DB structure)

package com.example.dto.search;

import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import java.util.ArrayList;

@JsonSerialize(using = BucketSerializer.class)
public class Bucket {
    private int docCount;
    private String key;
    // can be more specific if you have some superclass on top of all subcategories
    private List<Object> subCategoryElements = new ArrayList<>();
    private String nameOfSubcategory;

    // getters
}

And serializer class:

import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.SerializerProvider;
import com.fasterxml.jackson.databind.ser.std.StdSerializer;

import java.io.IOException;
import java.util.Optional;

public class BucketSerializer extends StdSerializer<Bucket> {

    public BucketSerializer() {
        this(null);
    }

    public BucketSerializer(Class<Bucket> t) {
        super(t);
    }

    @Override
    public void serialize(Bucket bucket, JsonGenerator gen, SerializerProvider provider) throws IOException {

        gen.writeStartObject();

        gen.writeNumberField("docCount", bucket.getDocCount());
        gen.writeStringField("key", bucket.getKey();
        gen.writeObjectField(Optional.ofNullable(bucket.getNameOfSubcategory()).orElse("unnamedCategory"), 
                  bucket.getSubCategoryElements());

        gen.writeEndObject();
    }
}

Maven dependency:

<dependency>
    <groupId>com.fasterxml.jackson.core</groupId>
    <artifactId>jackson-databind</artifactId>
    <version>2.8.8</version>
</dependency>

_____EDIT_1_____

I reproduced your case and have several suggestions.

Posting the code how I solved this problem:

Model:

// Aggregation
public class Aggregation {

    private Categories categories;

    public Categories getCategories() {
        return categories;
    }

    public void setCategories(Categories categories) {
        this.categories = categories;
    }
}

// Cetagories
import java.util.ArrayList;
import java.util.List;

public class Categories {

    private List<Bucket> buckets = new ArrayList<>();
    private int docCountErrorUpperBound;
    private int sumOtherDocCount;

    public List<Bucket> getBuckets() {
        return buckets;
    }

    public void setBuckets(List<Bucket> buckets) {
        this.buckets = buckets;
    }

    public int getDocCountErrorUpperBound() {
        return docCountErrorUpperBound;
    }

    public void setDocCountErrorUpperBound(int docCountErrorUpperBound) {
        this.docCountErrorUpperBound = docCountErrorUpperBound;
    }

    public int getSumOtherDocCount() {
        return sumOtherDocCount;
    }

    public void setSumOtherDocCount(int sumOtherDocCount) {
        this.sumOtherDocCount = sumOtherDocCount;
    }
}

//Bucket
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;

@JsonDeserialize(using = BucketDeserializer.class)
public class Bucket {

    private int docCount;
    private String key;
    private Categories subCategories;

    public int getDocCount() {
        return docCount;
    }

    public void setDocCount(int docCount) {
        this.docCount = docCount;
    }

    public String getKey() {
        return key;
    }

    public void setKey(String key) {
        this.key = key;
    }

    public Categories getSubCategories() {
        return subCategories;
    }

    public void setSubCategories(Categories subCategories) {
        this.subCategories = subCategories;
    }
}

Deserializer:

import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.deser.std.StdDeserializer;

import java.io.IOException;
import java.util.Arrays;
import java.util.Iterator;
import java.util.List;

public class BucketDeserializer extends StdDeserializer<Bucket> {

    public static final String DOC_COUNT = "doc_count";
    public static final String KEY = "key";
    public static final List<String> knownFieldNames = Arrays.asList(DOC_COUNT, KEY);

    public BucketDeserializer() {
        this(null);
    }

    public BucketDeserializer(Class<Bucket> c) {
        super(c);
    }

    @Override
    public Bucket deserialize(JsonParser jsonParser, DeserializationContext desContext) throws IOException {
        Bucket bucket = new Bucket();
        JsonNode jsonNode = jsonParser.getCodec().readTree(jsonParser);
        ObjectMapper objectMapper = new ObjectMapper();

        bucket.setDocCount(jsonNode.get(DOC_COUNT).asInt());
        bucket.setKey(jsonNode.get(KEY).asText());

        String unknownField = getUnknownField(jsonNode.fieldNames());
        if (unknownField != null)
            bucket.setSubCategories(objectMapper.convertValue(jsonNode.get(unknownField), Categories.class));

        return bucket;
    }

    public String getUnknownField(Iterator<String> fieldNames) {
        while (fieldNames.hasNext()) {
            String next = fieldNames.next();
            if (!knownFieldNames.contains(next))
                return next;
        }

        return null;
    }
}

The main idea was to find unknown/dynamic field/json key.

From JsonNode you can get all field names. I solved that declaring all known field names, and then find the field which is not member of this list. You can use switch as well for calling the setter by field name or create some another mapper. Also you can look at org.json.JSONObject class, which can make retrieve values by index number.

You cannot care about nested buckets, because this deserializer will handle them as well.

This is JSON request body I used:

{
    "categories": {
      "buckets": [
        {
          "doc_count": 12,
          "key": "IT",
          "it_category": {
            "buckets": [
              {
                "doc_count": 12,
                "key": "Programming"
              }
            ],
            "docCountErrorUpperBound": 0,
            "sumOtherDocCount": 0
          }
        },
        {
          "doc_count": 1,
          "key": "Handy Man",
          "plumb_category": {
            "buckets": [
              {
                "doc_count": 1,
                "key": "Plumbing"
              }
            ],
            "docCountErrorUpperBound": 0,
            "sumOtherDocCount": 0
          }
        }
      ],
      "docCountErrorUpperBound": 0,
      "sumOtherDocCount": 0
    }
}

And this is response I got:

{
  "categories": {
    "buckets": [
      {
        "docCount": 12,
        "key": "IT",
        "subCategories": {
          "buckets": [
            {
              "docCount": 12,
              "key": "Programming",
              "subCategories": null
            }
          ],
          "docCountErrorUpperBound": 0,
          "sumOtherDocCount": 0
        }
      },
      {
        "docCount": 1,
        "key": "Handy Man",
        "subCategories": {
          "buckets": [
            {
              "docCount": 1,
              "key": "Plumbing",
              "subCategories": null
            }
          ],
          "docCountErrorUpperBound": 0,
          "sumOtherDocCount": 0
        }
      }
    ],
    "docCountErrorUpperBound": 0,
    "sumOtherDocCount": 0
  }
}

The response is serialized with standard names because I didn't use any custom serializer. You can customise it as well using custom serializer I proposed in my original post.



来源:https://stackoverflow.com/questions/44203990/dto-from-json-with-dynamic-key

标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!