ฉันจะรวม JSON ดิบในวัตถุโดยใช้ Jackson ได้อย่างไร


103

ฉันกำลังพยายามรวม JSON ดิบไว้ในออบเจ็กต์ Java เมื่ออ็อบเจ็กต์ (de) ต่ออนุกรมโดยใช้ Jackson เพื่อทดสอบฟังก์ชันนี้ฉันได้เขียนการทดสอบต่อไปนี้:

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);
}

โค้ดแสดงบรรทัดต่อไปนี้:

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

JSON คือสิ่งที่ฉันต้องการ น่าเสียดายที่รหัสล้มเหลวโดยมีข้อยกเว้นเมื่อพยายามอ่าน JSON กลับไปที่ออบเจ็กต์ นี่คือข้อยกเว้น:

org.codehaus.jackson.map.JsonMappingException: ไม่สามารถ deserialize อินสแตนซ์ของ java.lang สตริงออกจากโทเค็น START_OBJECT ที่ [ที่มา: java.io.StringReader@d70d7a; บรรทัด: 1 คอลัมน์: 13] (ผ่านห่วงโซ่อ้างอิง: com.tnal.prism.cobalt.gather.testing.Pojo ["bar"])

ทำไมแจ็คสันถึงทำงานได้ดีในทิศทางเดียว แต่ล้มเหลวเมื่อไปอีกทิศทาง ดูเหมือนว่ามันควรจะสามารถเอาเอาต์พุตของตัวเองเป็นอินพุตได้อีกครั้ง ฉันรู้ว่าสิ่งที่ฉันพยายามทำนั้นนอกรีต (คำแนะนำทั่วไปคือการสร้างวัตถุภายในสำหรับbarสิ่งนั้นที่มีชื่อคุณสมบัติA) แต่ฉันไม่ต้องการโต้ตอบกับ JSON นี้เลย รหัสของฉันทำหน้าที่เป็นรหัสผ่านสำหรับรหัสนี้ - ฉันต้องการรับ JSON นี้และส่งกลับออกไปอีกครั้งโดยไม่ต้องสัมผัสอะไรเลยเพราะเมื่อ JSON เปลี่ยนแปลงฉันไม่ต้องการให้รหัสของฉันต้องแก้ไข

ขอบคุณสำหรับคำแนะนำ.

แก้ไข: ทำให้ Pojo เป็นคลาสแบบคงที่ซึ่งทำให้เกิดข้อผิดพลาดอื่น

คำตอบ:


64

@JsonRawValue มีไว้สำหรับการทำให้เป็นอนุกรมเท่านั้นเนื่องจากทิศทางย้อนกลับค่อนข้างยากที่จะจัดการ มีการเพิ่มเพื่ออนุญาตการแทรกเนื้อหาที่เข้ารหัสไว้ล่วงหน้า

ฉันเดาว่าเป็นไปได้ที่จะเพิ่มการสนับสนุนสำหรับการย้อนกลับแม้ว่าจะค่อนข้างอึดอัดก็ตาม: เนื้อหาจะต้องได้รับการแยกวิเคราะห์จากนั้นเขียนใหม่ในรูปแบบ "ดิบ" ซึ่งอาจจะเหมือนกันหรือไม่ก็ได้ (เนื่องจากการอ้างอักขระ อาจแตกต่างกัน) สำหรับกรณีทั่วไป แต่บางทีมันก็สมเหตุสมผลสำหรับปัญหาบางส่วน

แต่ฉันคิดว่าวิธีแก้ปัญหาสำหรับกรณีเฉพาะของคุณคือการระบุประเภทเป็น 'java.lang.Object' เนื่องจากสิ่งนี้ควรใช้งานได้: สำหรับการทำให้เป็นอนุกรมสตริงจะถูกส่งออกตามที่เป็นอยู่และสำหรับการ deserialization จะถูก deserialized เป็น แผนที่. จริงๆแล้วคุณอาจต้องการแยก getter / setter ถ้าเป็นเช่นนั้น getter จะส่งคืน String สำหรับการทำให้เป็นอนุกรม (และต้องการ @JsonRawValue); และ setter จะใช้ Map หรือ Object คุณสามารถเข้ารหัสอีกครั้งเป็น String ได้หากเหมาะสม


1
งานนี้เหมือนมีเสน่ห์ ดูคำตอบของฉันสำหรับรหัส ( การจัดรูปแบบในความคิดเห็นนั้นแย่มาก)
yves amsellem

ฉันมีกรณีการใช้งานที่แตกต่างออกไปสำหรับสิ่งนี้ ดูเหมือนว่าถ้าเราไม่ต้องการสร้างขยะสตริงจำนวนมากใน deser / ser เราควรจะสามารถส่งผ่านสตริงเช่นนี้ได้ ฉันเห็นชุดข้อความที่ติดตามสิ่งนี้ แต่ดูเหมือนว่าจะไม่มีการสนับสนุนแบบเนทีฟ ดูได้ที่markmail.org/message/…
Sid

@Sid ไม่มีวิธีใดที่จะทำและโทเค็นได้อย่างมีประสิทธิภาพ ในการรองรับการส่งผ่านของโทเค็นที่ยังไม่ได้ประมวลผลจะต้องมีการเก็บรักษาสถานะเพิ่มเติมซึ่งทำให้การแยกวิเคราะห์ "ปกติ" ค่อนข้างมีประสิทธิภาพน้อยกว่า มันเหมือนกับการเพิ่มประสิทธิภาพระหว่างรหัสปกติและการโยนข้อยกเว้น: เพื่อสนับสนุนหลังเพิ่มค่าโสหุ้ยสำหรับอดีต Jackson ไม่ได้ถูกออกแบบมาเพื่อพยายามให้อินพุตที่ยังไม่ได้ประมวลผล คงจะดี (สำหรับข้อความแสดงข้อผิดพลาดด้วย) แต่จะต้องใช้วิธีการที่แตกต่างออกไป
StaxMan

55

ต่อไปนี้@StaxManคำตอบผมได้ทำผลงานดังต่อไปนี้เช่นเสน่ห์:

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;
  }
}

และเพื่อให้ซื่อสัตย์กับคำถามเริ่มต้นนี่คือการทดสอบการทำงาน:

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
ฉันไม่ได้ลอง แต่ควรใช้งานได้: 1) สร้างโหนด JsonNode แทน Object json 2) ใช้ node.asText () แทน toString () ฉันไม่แน่ใจเกี่ยวกับอันที่ 2 แม้ว่า
Vadim Kirilchuk

ผมสงสัยว่าทำไมgetJson()ไม่กลับStringหลังจากทั้งหมด ถ้ามันส่งคืนค่าJsonNodeที่ตั้งไว้ผ่าน setter มันจะถูกทำให้เป็นอนุกรมตามที่ต้องการไม่ใช่เหรอ?
sorrymissjackson

@VadimKirilchuk ส่งกลับตรงข้ามค่าว่างnode.asText() toString()
v.ladynev

36

ฉันสามารถทำได้ด้วย deserializer ที่กำหนดเอง (ตัดและวางจากที่นี่ )

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
น่าทึ่งแค่ไหน IMO นี่ควรเป็นคำตอบอย่างเป็นทางการ ฉันลองใช้โครงสร้างที่ซับซ้อนมากซึ่งมีอาร์เรย์วัตถุย่อย ฯลฯ บางทีคุณอาจแก้ไขคำตอบของคุณและเพิ่มว่าสมาชิก String ที่จะ deserialized ควรมีคำอธิบายประกอบโดย @JsonDeserialize (โดยใช้ = KeepAsJsonDeserialzier.class) (และแก้ไขการพิมพ์ผิดในชื่อชั้นเรียนของคุณ
Heri

สิ่งนี้ใช้ได้กับ Deserializion แล้วการอนุกรมของ json ดิบเป็น pojo ล่ะ? จะสำเร็จได้อย่างไร
xtrakBandit

4
@xtrakBandit สำหรับ Serialization ให้ใช้ @JsonRawValue
smartwjw

การทำงานนี้เหมือนมีเสน่ห์ ขอบคุณ Roy และ @Heri. combination ของโพสต์นี้พร้อมกับความคิดเห็นของ Heri คือคำตอบที่ดีที่สุด
Michal

วิธีแก้ปัญหาที่ง่ายและเรียบร้อย เห็นด้วย @Heri
mahesh nanayakkara

18

@JsonSetter อาจช่วยได้ ดูตัวอย่างของฉัน ('data' ควรมี JSON ที่ไม่ได้แยกวิเคราะห์):

class Purchase
{
    String data;

    @JsonProperty("signature")
    String signature;

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

3
ตามวิธีการจัดทำเอกสาร JsonNode.toString () ที่จะสร้างการแสดงโหนดที่นักพัฒนาสามารถอ่านได้ ซึ่งอาจ <b> หรือไม่ </b> เป็น JSON ที่ถูกต้อง ดังนั้นนี่จึงเป็นการใช้งานที่มีความเสี่ยงมาก
Piotr

ตอนนี้ @Piotr javadoc บอกว่า "วิธีที่จะสร้าง JSON ที่ถูกต้อง (ณ Jackson 2.10) โดยใช้การตั้งค่าเริ่มต้นของ databind เป็น String"
bernie

4

การเพิ่มคำตอบที่ยอดเยี่ยมของRoy Trueloveนี่คือวิธีการฉีด deserialiser ที่กำหนดเองเพื่อตอบสนองต่อลักษณะของ:@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;
    }
}

สิ่งนี้ใช้ไม่ได้ใน Jackson 2.9 ดูเหมือนว่ามันจะพังเพราะตอนนี้ใช้คุณสมบัติเก่าใน PropertyBasedCreator.construct แทนการแทนที่หนึ่ง
dant3

3

นี่เป็นปัญหากับชั้นเรียนภายในของคุณ Pojoระดับคือระดับชั้นไม่คงที่ของการเรียนการทดสอบของคุณแจ็คสันและไม่สามารถ instantiate ว่าชั้น ดังนั้นจึงสามารถทำให้เป็นอนุกรมได้ แต่ไม่สามารถทำให้เป็นซีเรียลไลซ์ได้

กำหนดคลาสของคุณใหม่เช่นนี้:

public static class Pojo {
    public String foo;

    @JsonRawValue
    public String bar;
}

หมายเหตุการเพิ่มของ static


ขอบคุณ. นั่นทำให้ฉันก้าวไปอีกขั้น แต่ตอนนี้ฉันได้รับข้อผิดพลาดที่แตกต่างออกไป ฉันจะอัปเดตโพสต์เดิมพร้อมข้อผิดพลาดใหม่
bhilstrom

3

วิธีง่ายๆนี้ใช้ได้ผลสำหรับฉัน:

public class MyObject {
    private Object rawJsonValue;

    public Object getRawJsonValue() {
        return rawJsonValue;
    }

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

ดังนั้นฉันจึงสามารถจัดเก็บค่าดิบของ JSON ในตัวแปร rawJsonValue ได้จากนั้นก็ไม่มีปัญหาในการยกเลิกการกำหนดค่า (เป็นวัตถุ) ด้วยฟิลด์อื่น ๆ กลับไปที่ JSON และส่งผ่าน REST ของฉัน การใช้ @JsonRawValue ไม่ได้ช่วยฉันเพราะ JSON ที่จัดเก็บไว้ถูก deserialized เป็น String ไม่ใช่วัตถุและนั่นไม่ใช่สิ่งที่ฉันต้องการ


3

สิ่งนี้ใช้ได้แม้ในเอนทิตี 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());
}

ตามหลักการแล้ว ObjectMapper และแม้แต่ JsonFactory นั้นมาจากบริบทและได้รับการกำหนดค่าเพื่อจัดการ JSON ของคุณอย่างถูกต้อง (มาตรฐานหรือด้วยค่าที่ไม่เป็นมาตรฐานเช่นการลอยตัว 'Infinity')


1
ตามJsonNode.toString()เอกสารMethod that will produce developer-readable representation of the node; which may <b>or may not</b> be as valid JSON.ดังนั้นนี่เป็นการใช้งานที่มีความเสี่ยงมาก
Piotr

สวัสดี @Piotr ขอบคุณสำหรับคำใบ้ แน่นอนคุณพูดถูกสิ่งนี้ใช้JsonNode.asText()ภายในและจะแสดงผล Infinity และค่า JSON อื่น ๆ ที่ไม่ได้มาตรฐาน
เฟรด

ตอนนี้ @Piotr javadoc บอกว่า "วิธีที่จะสร้าง JSON ที่ถูกต้อง (ณ Jackson 2.10) โดยใช้การตั้งค่าเริ่มต้นของ databind เป็น String"
bernie

2

นี่คือตัวอย่างการทำงานที่สมบูรณ์ของวิธีการใช้โมดูล Jackson เพื่อให้@JsonRawValueทำงานได้ทั้งสองวิธี (การทำให้เป็นอนุกรมและการแยกส่วนย่อย):

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();
        }
    }
}

จากนั้นคุณสามารถลงทะเบียนโมดูลหลังจากสร้างObjectMapper:

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

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

มีอะไรอีกบ้างนอกเหนือจากข้างต้นที่คุณต้องทำ? ฉันพบว่าวิธีการ deserialize ของ JsonRawValueDeserializer ไม่เคยถูกเรียกโดย ObjectMapper
Michael Coxon

@MichaelCoxon คุณจัดการเพื่อให้มันทำงานได้หรือไม่? สิ่งหนึ่งที่ทำให้ฉันมีปัญหาในอดีตคือการใช้คำอธิบายประกอบจากorg.codehaus.jacksonแพ็คเกจโดยไม่รู้ตัว com.fasterxml.jacksonตรวจสอบให้แน่ใจว่าการนำเข้าของคุณทั้งหมดมาจาก
Helder Pereira

1

ฉันมีปัญหาเดียวกันแน่นอน ฉันพบวิธีแก้ปัญหาในโพสต์นี้: แยกวิเคราะห์โครงสร้าง JSON เป็นคลาสธรรมดาโดยใช้ Jackson หรือทางเลือกอื่น

ตรวจสอบคำตอบสุดท้าย ด้วยการกำหนด setter แบบกำหนดเองสำหรับคุณสมบัติที่ใช้ JsonNode เป็นพารามิเตอร์และเรียกใช้เมธอด toString บน jsonNode เพื่อตั้งค่าคุณสมบัติ String ทุกอย่างจะทำงานได้


1

การใช้ออบเจ็กต์ใช้ได้ผลดีทั้งสองวิธี ...

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

ฉันมีปัญหาคล้ายกัน แต่ใช้รายการที่มี JSON itens ( List<String>) จำนวนมาก

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

ฉันจัดการการทำให้เป็นอนุกรมโดยใช้@JsonRawValueคำอธิบายประกอบ แต่สำหรับ deserialization ฉันต้องสร้างdeserializer ที่กำหนดเองตามคำแนะนำของ Roy

public class Errors {

    private Integer status;

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

}

ด้านล่างนี้คุณสามารถดู deserializer "รายการ" ของฉัน

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");
    }
}
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.