Làm cách nào tôi có thể đưa JSON thô vào một đối tượng bằng cách sử dụng Jackson?


102

Tôi đang cố gắng đưa JSON thô vào bên trong một đối tượng Java khi đối tượng được (de) tuần tự hóa bằng cách sử dụng Jackson. Để kiểm tra chức năng này, tôi đã viết bài kiểm tra sau:

public static class Pojo {
    public String foo;

    @JsonRawValue
    public String bar;
}

@Test
public void test() throws JsonGenerationException, JsonMappingException, IOException {

    String foo = "one";
    String bar = "{\"A\":false}";

    Pojo pojo = new Pojo();
    pojo.foo = foo;
    pojo.bar = bar;

    String json = "{\"foo\":\"" + foo + "\",\"bar\":" + bar + "}";

    ObjectMapper objectMapper = new ObjectMapper();
    String output = objectMapper.writeValueAsString(pojo);
    System.out.println(output);
    assertEquals(json, output);

    Pojo deserialized = objectMapper.readValue(output, Pojo.class);
    assertEquals(foo, deserialized.foo);
    assertEquals(bar, deserialized.bar);
}

Mã xuất ra dòng sau:

{"foo":"one","bar":{"A":false}}

JSON chính xác là cách tôi muốn mọi thứ trông như thế nào. Thật không may, mã không thành công với một ngoại lệ khi cố gắng đọc lại JSON vào đối tượng. Đây là ngoại lệ:

org.codehaus.jackson.map.JsonMappingException: Không thể giải mã hóa phiên bản java.lang.String ra khỏi mã thông báo START_OBJECT tại [Nguồn: java.io.StringReader@d70d7a; dòng: 1, cột: 13] (thông qua chuỗi tham chiếu: com.tnal.prism.cobalt.gather.testing.Pojo ["bar"])

Tại sao Jackson hoạt động tốt ở một hướng nhưng lại thất bại khi đi theo hướng khác? Có vẻ như nó sẽ có thể lấy lại đầu ra của chính nó làm đầu vào. Tôi biết những gì tôi đang cố gắng làm là không chính thống (lời khuyên chung là tạo một đối tượng bên trong barcó thuộc tính được đặt tên A), nhưng tôi không muốn tương tác với JSON này. Mã của tôi đang hoạt động như một mã chuyển tiếp cho mã này - Tôi muốn lấy JSON này và gửi lại nó một lần nữa mà không cần chạm vào bất kỳ thứ gì, bởi vì khi JSON thay đổi, tôi không muốn mã của mình cần sửa đổi.

Cảm ơn vì lời khuyên.

CHỈNH SỬA: Đã tạo cho Pojo một lớp tĩnh, lớp này đang gây ra một lỗi khác.

Câu trả lời:


64

@JsonRawValue chỉ dành cho phía tuần tự hóa, vì hướng ngược lại khó xử lý hơn một chút. Trên thực tế, nó đã được thêm vào để cho phép đưa nội dung được mã hóa trước vào.

Tôi đoán sẽ có thể thêm hỗ trợ cho đảo ngược, mặc dù điều đó sẽ khá khó xử: nội dung sẽ phải được phân tích cú pháp và sau đó được viết lại về dạng "thô", có thể giống hoặc có thể không giống nhau (kể từ khi trích dẫn ký tự có thể khác nhau). Điều này cho trường hợp chung. Nhưng có lẽ nó sẽ có ý nghĩa đối với một số vấn đề nhỏ.

Nhưng tôi nghĩ rằng một giải pháp xung quanh cho trường hợp cụ thể của bạn sẽ là chỉ định loại là 'java.lang.Object', vì điều này sẽ hoạt động tốt: đối với tuần tự hóa, Chuỗi sẽ được xuất như hiện tại và đối với giải mã hóa, nó sẽ được giải mã hóa như Bản đồ. Trên thực tế, bạn có thể muốn có getter / setter riêng biệt nếu vậy; getter sẽ trả về Chuỗi để tuần tự hóa (và cần @JsonRawValue); và setter sẽ lấy Bản đồ hoặc Đối tượng. Bạn có thể mã hóa lại nó thành Chuỗi nếu điều đó hợp lý.


1
Công việc này như một cái duyên vậy; xem phản hồi của tôi cho mã ( định dạng trong các nhận xét là awgul ).
Yves Amsellem

Tôi đã có một trường hợp sử dụng khác cho việc này. Có vẻ như nếu chúng ta không muốn tạo ra nhiều chuỗi rác trong dãy / ser, chúng ta có thể chuyển qua một chuỗi như vậy. Tôi đã thấy một chuỗi theo dõi điều này, nhưng có vẻ như không có hỗ trợ gốc nào có thể. Hãy xem markmail.org/message/…
Sid

@Sid không có cách nào để làm điều đó VÀ mã hóa cả hai hiệu quả. Để hỗ trợ chuyển các mã thông báo chưa được xử lý sẽ yêu cầu lưu giữ trạng thái bổ sung, điều này làm cho việc phân tích cú pháp "thông thường" hơi kém hiệu quả hơn. Nó giống như tối ưu hóa giữa mã thông thường và ném ngoại lệ: để hỗ trợ cái sau thêm chi phí cho cái trước. Jackson không được thiết kế để cố gắng cung cấp đầu vào chưa xử lý; thật tuyệt nếu có nó (đối với cả thông báo lỗi), nhưng sẽ yêu cầu cách tiếp cận khác.
StaxMan

55

Sau câu trả lời của @StaxMan , tôi đã thực hiện các tác phẩm sau như một sự quyến rũ:

public class Pojo {
  Object json;

  @JsonRawValue
  public String getJson() {
    // default raw value: null or "[]"
    return json == null ? null : json.toString();
  }

  public void setJson(JsonNode node) {
    this.json = node;
  }
}

Và, để trung thành với câu hỏi ban đầu, đây là bài kiểm tra hoạt động:

public class PojoTest {
  ObjectMapper mapper = new ObjectMapper();

  @Test
  public void test() throws IOException {
    Pojo pojo = new Pojo("{\"foo\":18}");

    String output = mapper.writeValueAsString(pojo);
    assertThat(output).isEqualTo("{\"json\":{\"foo\":18}}");

    Pojo deserialized = mapper.readValue(output, Pojo.class);
    assertThat(deserialized.json.toString()).isEqualTo("{\"foo\":18}");
    // deserialized.json == {"foo":18}
  }
}

1
Tôi đã không thử, nhưng nó sẽ hoạt động: 1) tạo một nút JsonNode thay vì Đối tượng json 2) sử dụng node.asText () thay vì toString (). Tôi không chắc chắn về cái thứ 2 mặc dù.
Vadim Kirilchuk

Tôi tự hỏi tại sao getJson()lại quay trở lại Stringsau khi tất cả. Nếu nó chỉ trả về giá trị JsonNodeđã được thiết lập thông qua bộ cài đặt, nó sẽ được nối tiếp như mong muốn, phải không?
sorrymissjackson

@VadimKirilchuk node.asText()trả về giá trị trống đối diện toString().
v.ladynev

36

Tôi đã có thể làm điều này với một bộ khử không khí tùy chỉnh (cắt và dán từ đây )

package etc;

import java.io.IOException;

import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.TreeNode;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JsonDeserializer;

/**
 * Keeps json value as json, does not try to deserialize it
 * @author roytruelove
 *
 */
public class KeepAsJsonDeserialzier extends JsonDeserializer<String> {

    @Override
    public String deserialize(JsonParser jp, DeserializationContext ctxt)
            throws IOException, JsonProcessingException {

        TreeNode tree = jp.getCodec().readTree(jp);
        return tree.toString();
    }
}

6
Thật tuyệt vời làm sao đơn giản. IMO đây phải là câu trả lời chính thức. Tôi đã thử với một cấu trúc rất phức tạp có chứa các mảng, subobjects, v.v. Có thể bạn chỉnh sửa câu trả lời của mình và thêm rằng thành viên Chuỗi cần được deserialized nên được chú thích bởi @JsonDeserialize (using = KeepAsJsonDeserialzier.class). (và sửa lỗi đánh máy trong tên lớp của bạn ;-)
Heri

điều này hoạt động cho Deserializion. Làm thế nào về Serialization của json thô thành một pojo? Làm thế nào mà có thể được thực hiện
xtrakBandit

4
@xtrakBandit cho serialization, sử dụng@JsonRawValue
smartwjw

Công việc này như một cái duyên vậy. Cảm ơn Roy và @Heri ..com. Sự kết hợp của bài đăng này cùng với bình luận của Heri là câu trả lời hay nhất.
Michal

Giải pháp đơn giản và gọn gàng. Tôi đồng ý với @Heri
mahesh nanayakkara

18

@JsonSetter có thể giúp bạn. Xem mẫu của tôi ('dữ liệu' được cho là chứa JSON chưa được phân tích cú pháp):

class Purchase
{
    String data;

    @JsonProperty("signature")
    String signature;

    @JsonSetter("data")
    void setData(JsonNode data)
    {
        this.data = data.toString();
    }
}

3
Theo tài liệu JsonNode.toString () Phương pháp sẽ tạo ra biểu diễn nút có thể đọc được của nhà phát triển; có thể <b> hoặc không </b> là JSON hợp lệ. Vì vậy, đây thực sự là việc thực hiện rất rủi ro.
Piotr

@Piotr javadoc hiện nói "Phương pháp sẽ tạo ra (kể từ Jackson 2.10) JSON hợp lệ bằng cách sử dụng cài đặt mặc định của databind, dưới dạng Chuỗi"
bernie

4

Thêm vào câu trả lời tuyệt vời của Roy Truelove , đây là cách tiêm bộ khử gió tùy chỉnh để đáp ứng với sự xuất hiện của :@JsonRawValue

import com.fasterxml.jackson.databind.Module;

@Component
public class ModuleImpl extends Module {

    @Override
    public void setupModule(SetupContext context) {
        context.addBeanDeserializerModifier(new BeanDeserializerModifierImpl());
    }
}

import java.util.Iterator;

import com.fasterxml.jackson.annotation.JsonRawValue;
import com.fasterxml.jackson.databind.BeanDescription;
import com.fasterxml.jackson.databind.DeserializationConfig;
import com.fasterxml.jackson.databind.deser.BeanDeserializerBuilder;
import com.fasterxml.jackson.databind.deser.BeanDeserializerModifier;
import com.fasterxml.jackson.databind.deser.SettableBeanProperty;

public class BeanDeserializerModifierImpl extends BeanDeserializerModifier {
    @Override
    public BeanDeserializerBuilder updateBuilder(DeserializationConfig config, BeanDescription beanDesc, BeanDeserializerBuilder builder) {
        Iterator<SettableBeanProperty> it = builder.getProperties();
        while (it.hasNext()) {
            SettableBeanProperty p = it.next();
            if (p.getAnnotation(JsonRawValue.class) != null) {
                builder.addOrReplaceProperty(p.withValueDeserializer(KeepAsJsonDeserialzier.INSTANCE), true);
            }
        }
        return builder;
    }
}

điều này không hoạt động trong Jackson 2.9. Trông giống như nó đã bị hỏng như nó bây giờ sử dụng tài sản cũ ở PropertyBasedCreator.construct thay vì thay thế một
dant3

3

Đây là một vấn đề với các lớp bên trong của bạn. Các Pojolớp là một lớp bên trong không tĩnh của lớp thử nghiệm của bạn, và Jackson có thể không nhanh chóng rằng lớp. Vì vậy, nó có thể tuần tự hóa, nhưng không giải mã hóa.

Định nghĩa lại lớp của bạn như thế này:

public static class Pojo {
    public String foo;

    @JsonRawValue
    public String bar;
}

Lưu ý việc bổ sung static


Cảm ơn. Điều đó đã giúp tôi tiến thêm một bước nữa, nhưng giờ tôi lại mắc phải một lỗi khác. Tôi sẽ cập nhật bài đăng gốc với lỗi mới.
bhilstrom

3

Giải pháp dễ dàng này đã làm việc cho tôi:

public class MyObject {
    private Object rawJsonValue;

    public Object getRawJsonValue() {
        return rawJsonValue;
    }

    public void setRawJsonValue(Object rawJsonValue) {
        this.rawJsonValue = rawJsonValue;
    }
}

Vì vậy, tôi đã có thể lưu trữ giá trị thô của JSON trong biến rawJsonValue và sau đó không có vấn đề gì khi giải mã hóa nó (dưới dạng đối tượng) với các trường khác trở lại JSON và gửi qua REST của tôi. Việc sử dụng @JsonRawValue không giúp ích được gì cho tôi vì JSON được lưu trữ đã được deserialized dưới dạng Chuỗi, không phải dưới dạng đối tượng và đó không phải là điều tôi muốn.


3

Điều này thậm chí hoạt động trong một thực thể JPA:

private String json;

@JsonRawValue
public String getJson() {
    return json;
}

public void setJson(final String json) {
    this.json = json;
}

@JsonProperty(value = "json")
public void setJsonRaw(JsonNode jsonNode) {
    // this leads to non-standard json, see discussion: 
    // setJson(jsonNode.toString());

    StringWriter stringWriter = new StringWriter();
    ObjectMapper objectMapper = new ObjectMapper();
    JsonGenerator generator = 
      new JsonFactory(objectMapper).createGenerator(stringWriter);
    generator.writeTree(n);
    setJson(stringWriter.toString());
}

Lý tưởng nhất là ObjectMapper và thậm chí JsonFactory là từ ngữ cảnh và được định cấu hình để xử lý JSON của bạn một cách chính xác (tiêu chuẩn hoặc với các giá trị không tiêu chuẩn như phao 'Infinity' chẳng hạn).


1
Theo JsonNode.toString()tài liệu Method that will produce developer-readable representation of the node; which may <b>or may not</b> be as valid JSON.Vì vậy, đây thực sự là việc thực hiện rất rủi ro.
Piotr

Xin chào @Piotr, cảm ơn vì gợi ý. Tất nhiên, bạn đúng, điều này sử dụng JsonNode.asText()nội bộ và sẽ xuất ra giá trị Infinity và các giá trị JSON không chuẩn khác.
Georg

@Piotr javadoc hiện nói "Phương pháp sẽ tạo ra (kể từ Jackson 2.10) JSON hợp lệ bằng cách sử dụng cài đặt mặc định của databind, dưới dạng Chuỗi"
bernie

2

Dưới đây là một ví dụ hoạt động đầy đủ về cách sử dụng mô-đun Jackson để làm @JsonRawValueviệc theo cả hai cách (tuần tự hóa và giải mã hóa):

public class JsonRawValueDeserializerModule extends SimpleModule {

    public JsonRawValueDeserializerModule() {
        setDeserializerModifier(new JsonRawValueDeserializerModifier());
    }

    private static class JsonRawValueDeserializerModifier extends BeanDeserializerModifier {
        @Override
        public BeanDeserializerBuilder updateBuilder(DeserializationConfig config, BeanDescription beanDesc, BeanDeserializerBuilder builder) {
            builder.getProperties().forEachRemaining(property -> {
                if (property.getAnnotation(JsonRawValue.class) != null) {
                    builder.addOrReplaceProperty(property.withValueDeserializer(JsonRawValueDeserializer.INSTANCE), true);
                }
            });
            return builder;
        }
    }

    private static class JsonRawValueDeserializer extends JsonDeserializer<String> {
        private static final JsonDeserializer<String> INSTANCE = new JsonRawValueDeserializer();

        @Override
        public String deserialize(JsonParser p, DeserializationContext ctxt) throws IOException, JsonProcessingException {
            return p.readValueAsTree().toString();
        }
    }
}

Sau đó, bạn có thể đăng ký mô-đun sau khi tạo ObjectMapper:

ObjectMapper objectMapper = new ObjectMapper();
objectMapper.registerModule(new JsonRawValueDeserializerModule());

String json = "{\"foo\":\"one\",\"bar\":{\"A\":false}}";
Pojo deserialized = objectMapper.readValue(json, Pojo.class);

Có điều gì khác ngoài những điều trên mà bạn phải làm không? Tôi đã tìm thấy rằng phương pháp deserialize của JsonRawValueDeserializer không bao giờ được gọi bởi các ObjectMapper
Michael Coxon

@MichaelCoxon Bạn đã quản lý để làm cho nó hoạt động? Một điều khiến tôi gặp sự cố trong quá khứ là sử dụng chú thích từ org.codehaus.jacksongói mà không nhận ra nó. Đảm bảo rằng tất cả hàng nhập khẩu của bạn đều đến từ com.fasterxml.jackson.
Helder Pereira

1

Tôi đã có vấn đề chính xác cùng. Tôi đã tìm thấy giải pháp trong bài đăng này: Phân tích cú pháp cây JSON thành lớp đơn giản bằng cách sử dụng Jackson hoặc các lựa chọn thay thế của nó

Kiểm tra câu trả lời cuối cùng. Bằng cách xác định bộ thiết lập tùy chỉnh cho thuộc tính lấy JsonNode làm tham số và gọi phương thức toString trên jsonNode để đặt thuộc tính String, tất cả đều hoạt động.


1

Sử dụng một đối tượng hoạt động tốt theo cả hai cách ... Phương pháp này có một chút chi phí làm giảm giá trị thô trong hai lần.

ObjectMapper mapper = new ObjectMapper();
RawJsonValue value = new RawJsonValue();
value.setRawValue(new RawHello(){{this.data = "universe...";}});
String json = mapper.writeValueAsString(value);
System.out.println(json);
RawJsonValue result = mapper.readValue(json, RawJsonValue.class);
json = mapper.writeValueAsString(result.getRawValue());
System.out.println(json);
RawHello hello = mapper.readValue(json, RawHello.class);
System.out.println(hello.data);

RawHello.java

public class RawHello {

    public String data;
}

RawJsonValue.java

public class RawJsonValue {

    private Object rawValue;

    public Object getRawValue() {
        return rawValue;
    }

    public void setRawValue(Object value) {
        this.rawValue = value;
    }
}

1

Tôi đã gặp sự cố tương tự nhưng sử dụng danh sách có nhiều vòng lặp JSON ( List<String>).

public class Errors {
    private Integer status;
    private List<String> jsons;
}

Tôi quản lý tuần tự hóa bằng cách sử dụng @JsonRawValuechú thích. Nhưng để khử không khí, tôi phải tạo một bộ khử không khí tùy chỉnh dựa trên đề xuất của Roy.

public class Errors {

    private Integer status;

    @JsonRawValue
    @JsonDeserialize(using = JsonListPassThroughDeserialzier.class)
    private List<String> jsons;

}

Dưới đây, bạn có thể thấy bộ khử không khí "Danh sách" của tôi.

public class JsonListPassThroughDeserializer extends JsonDeserializer<List<String>> {

    @Override
    public List<String> deserialize(JsonParser jp, DeserializationContext cxt) throws IOException, JsonProcessingException {
        if (jp.getCurrentToken() == JsonToken.START_ARRAY) {
            final List<String> list = new ArrayList<>();
            while (jp.nextToken() != JsonToken.END_ARRAY) {
                list.add(jp.getCodec().readTree(jp).toString());
            }
            return list;
        }
        throw cxt.instantiationException(List.class, "Expected Json list");
    }
}
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.