@JsonTypeResolver是解析使用多个属性的唯一选项吗?

时间:2015-05-05 18:29:22

标签: java json jackson

我有以下格式的传入JSON数据

{
    "header": {
        "schema_id": {
            "namespace": "omh",
            "name": "physical-activity",
        },
    },
    "body": {
        "activity_name": "walking",
        "distance": {
            "value": 1.5,
            "unit": "mi"
        },
    }
}

和相应的类似

的Java类
public class DataPoint<T extends Measure> {

    private DataPointHeader header;
    private T body;

@JsonNaming(LowerCaseWithUnderscoresStrategy.class)
public class PhysicalActivity extends Measure {

    private String activityName;
    private LengthUnitValue distance;

我希望杰克逊根据JSON文档中的bodyPhysicalActivity解析为schema_id类型,例如:在伪代码中

if schema_id.namespace == 'omh' && schema_id.name == 'physical-activity'
     then return PhysicalActivity.class

我已尝试使用@JsonTypeIdResolver执行此操作,但如果我尝试使用header.schema_id.name导航到@JsonTypeInfo,例如

@JsonTypeInfo(use = JsonTypeInfo.Id.CUSTOM,
        include = JsonTypeInfo.As.EXTERNAL_PROPERTY,
        property = "header.schema_id.name")
@JsonTypeIdResolver(DataPointTypeIdResolver.class)
public abstract class Measure {

我收到missing property: 'header.schema_id.name'错误。即使我可以,我也不认为我可以对namespacename属性做出决定。

除了使用@JsonTypeResolver

从头开始构建之外,还有一种理智的方法吗?

2 个答案:

答案 0 :(得分:2)

在杰克逊源代码中似乎有很多假设类型ID是字符串,所以我怀疑JsonTypeResolver是一种方法......但它当然看起来并不简单!

至少当你只有'header'和'body'属性时,一个完全自定义的反序列化器并不太难:

public static class DataPointDeserializer extends StdDeserializer<DataPoint<?>> implements ResolvableDeserializer {
    private JsonDeserializer<Object> headerDeserializer;
    private Map<SchemaId, JsonDeserializer<Object>> activityDeserializers;

    public DataPointDeserializer() {
        super(DataPoint.class);
    }

    @Override
    public void resolve(DeserializationContext ctxt) throws JsonMappingException {
        headerDeserializer = ctxt.findRootValueDeserializer(ctxt.getTypeFactory().constructType(
                DataPointHeader.class));
        activityDeserializers = new HashMap<>();
        activityDeserializers.put(new SchemaId("omh", "physical-activity"),
                ctxt.findRootValueDeserializer(ctxt.getTypeFactory().constructType(PhysicalActivity.class)));
    }

    @Override
    public DataPoint<?> deserialize(JsonParser p, DeserializationContext ctxt) throws IOException,
            JsonProcessingException {
        String fieldName = p.nextFieldName();
        if (fieldName == null)
            throw ctxt.wrongTokenException(p, JsonToken.FIELD_NAME, "expected 'header' and 'body' fields");
        if (fieldName.equals("header")) {
            p.nextToken();
            DataPointHeader header = (DataPointHeader) headerDeserializer.deserialize(p, ctxt);
            JsonDeserializer<Object> bodyDeserializer = activityDeserializers.get(header.schemaId);
            if (bodyDeserializer == null) throw ctxt.mappingException("No mapping for schema: " + header.schemaId);
            fieldName = p.nextFieldName();
            if (fieldName == null)
                throw ctxt.wrongTokenException(p, JsonToken.FIELD_NAME, "expected 'body' field after header");
            p.nextToken();
            Measure body = (Measure) bodyDeserializer.deserialize(p, ctxt);
            DataPoint<Measure> dataPoint = new DataPoint<>();
            dataPoint.header = header;
            dataPoint.body = body;
            return dataPoint;
        }
        else if (fieldName.equals("body")) {
            p.nextToken();
            try (TokenBuffer tb = new TokenBuffer(p)) {
                tb.copyCurrentStructure(p);
                fieldName = p.nextFieldName();
                if (fieldName == null)
                    throw ctxt.wrongTokenException(p, JsonToken.FIELD_NAME, "expected 'header' field after body");
                if (!fieldName.equals("header"))
                    throw ctxt.weirdStringException(fieldName, DataPoint.class, "Unexpected field name");
                p.nextToken();
                DataPointHeader header = (DataPointHeader) headerDeserializer.deserialize(p, ctxt);
                JsonDeserializer<Object> bodyDeserializer = activityDeserializers.get(header.schemaId);
                if (bodyDeserializer == null)
                    throw ctxt.mappingException("No mapping for schema: " + header.schemaId);
                JsonParser bodyParser = tb.asParser();
                bodyParser.nextToken();
                Measure body = (Measure) bodyDeserializer.deserialize(bodyParser, ctxt);
                DataPoint<Measure> dataPoint = new DataPoint<>();
                dataPoint.header = header;
                dataPoint.body = body;
                return dataPoint;
            }
        }
        else throw ctxt.weirdStringException(fieldName, DataPoint.class, "Unexpected field name");
    }
}

答案 1 :(得分:2)

不,没有办法使用路径表达式来匹配属性。这将需要访问完整的JSON(子树)。

对于Jackson 2.5,由于类型ID的要求是标量值(通常是字符串),因此可以轻微放松,因此可以支持JSOG。更多背景是关于这个问题:

https://github.com/FasterXML/jackson-databind/issues/622

但我认为这远远不足以让你使用标准的杰克逊式ID分辨率。

您可能会考虑的一件事是创作者方法,例如:

abstract class Measure {
   // either constructor, or static method:
   @JsonCreator
   public static Measure construct(
     @JsonProperty("header") HeadOb header, // or JsonNode, Map etc
     @JsonProperty("body") JsonNode body) {
        // extract type info, build actual instance from body
     }
}

或者,可能是Converter,其中您使用中间包装器来绑定标头,并且主体仅绑定到JsonNodeMap,然后从那里构造。