ObjectMapper?
Jackson ๋ผ์ด๋ธ๋ฌ๋ฆฌ ํด๋์ค ์ค ํ๋์ธ ObjectMapper ํตํด JSON → Object
(์ญ์ง๋ ฌํ), Object → JSON
(์ง๋ ฌํ)๋ฅผ ๊ฐ๋จํ๊ฒ ํ์ฑํ ์ ์๋ ๋ผ์ด๋ธ๋ฌ๋ฆฌ ์
๋๋ค.
readValue()
- test.json
{
"name" : "john",
"age" : 20
}
json → Object
๋น์ง๋ ฌํ ์ฝ๋
People people = objectMapper.readValue(new File("static/test.json"), People.class);
people์ ๊ฐ ๊ฐ์ ์ฐ์ด๋ณด๋ฉด ์ ์ถ๋ ฅ๋ฉ๋๋ค.
System.out.println(people.getName() + ", " + people.getAge());
// john, 20
๊ทธ๋ฐ๋ฐ ๋ง์ ๋๋ค.. People์ ์ฝ๋๋ฅผ ํ๋ฒ ๋ณผ๊น์..?
@Getter
@NoArgsConstructor
@AllArgsConstructor
public class People {
private String name;
private int age;
}
??? Setter๊ฐ ์์ต๋๋ค.. ๊ทธ๋ผ ์ด๋ป๊ฒ ๊ฐ์ ๋ฃ์ด์ค๊ฑธ๊น์..?
(์ ๋ Setter๋ฅผ ํตํด์ ๊ฐ์ ๋ฃ์ด์ฃผ๋ ๊ฑธ๋ก ์๊ณ ์์์ต๋๋ค๋ง..)
์๋์์ ์ฒ์ฒํ ํ๋ฒ ์์๋ณด๊ฒ ์ต๋๋ค.
๋์ ๋ฐฉ์
protected JavaType _fromAny(ClassStack context, Type srcType, TypeBindings bindings)
{
JavaType resultType;
// simple class?
if (srcType instanceof Class<?>) {
// Important: remove possible bindings since this is type-erased thingy
resultType = _fromClass(context, (Class<?>) srcType, EMPTY_BINDINGS);
}
- ํด๋น ํด๋์ค ํ์ ์ ๋จผ์ ์ฐพ์ต๋๋ค.
ํต์ฌ ๋ถ๋ถ
๋น์ง๋ ฌํ ํ ๋, ๊ธฐ๋ณธ ์ฌ์ฉ์๋ ํ์์ ๋๋ค. ์๋๊ตฌ์?
BeanDeserializer.java
@Override
public Object deserialize(JsonParser p, DeserializationContext ctxt) throws IOException
{
// common case first
if (p.isExpectedStartObjectToken()) {
if (_vanillaProcessing) {
return vanillaDeserialize(p, ctxt, p.nextToken()); // (1)
}
// 23-Sep-2015, tatu: This is wrong at some many levels, but for now... it is
// what it is, including "expected behavior".
p.nextToken();
if (_objectIdReader != null) {
return deserializeWithObjectId(p, ctxt);
}
return deserializeFromObject(p, ctxt);
}
return _deserializeOther(p, ctxt, p.currentToken());
}
(1) Json์ ํ์ฑํ Parser์, DeserializationContext๋ฅผ ๊ฐ์ง๊ณ vanillaDeserialize๋ฅผ ํธ์ถํฉ๋๋ค.
vanillaDeserializer()
private final Object vanillaDeserialize(JsonParser p,
DeserializationContext ctxt, JsonToken t)
throws IOException
{
final Object bean = _valueInstantiator.createUsingDefault(ctxt); // (1)
// [databind#631]: Assign current value, to be accessible by custom serializers
p.setCurrentValue(bean);
if (p.hasTokenId(JsonTokenId.ID_FIELD_NAME)) {
String propName = p.currentName(); // (2)
do {
p.nextToken();
SettableBeanProperty prop = _beanProperties.find(propName); // (3)
if (prop != null) { // normal case
try {
prop.deserializeAndSet(p, ctxt, bean); // (4)
} catch (Exception e) {
wrapAndThrow(e, bean, propName, ctxt);
}
continue;
}
handleUnknownVanilla(p, ctxt, bean, propName);
} while ((propName = p.nextFieldName()) != null); // (5)
}
return bean;
}
(1) createUsingDefault๋ฅผ ํธ์ถํด ๊ธฐ๋ณธ ์์ฑ์๋ฅผ ํตํด ์ธ์คํด์ค๋ฅผ ์์ฑํฉ๋๋ค.
AnnotatedConstructor.java
@Override
public Object createUsingDefault(DeserializationContext ctxt) throws IOException
{
if (_defaultCreator == null) { // sanity-check; caller should check
return super.createUsingDefault(ctxt);
}
try {
return _defaultCreator.call(); // (1-A)
} catch (Exception e) { // 19-Apr-2017, tatu: Let's not catch Errors, just Exceptions
return ctxt.handleInstantiationProblem(_valueClass, null, rewrapCtorProblem(ctxt, e));
}
}
(1-A) _defaultCreator๋ฅผ call() ํ๋ ๊ฒ์ ๋ณผ ์ ์์ต๋๋ค.
StdValueInstantiator.java
@Override
public final Object call() throws Exception {
// 31-Mar-2021, tatu: Note! This is faster than calling without arguments
// because JDK in its wisdom would otherwise allocate `new Object[0]` to pass
return _constructor.newInstance((Object[]) null); // (1-B)
}
(1-B) ๋๋ฒ๊น ์ ํด๋ณด๋ฉด ๊ธฐ๋ณธ ์์ฑ์๋ฅผ ํธ์ถํ๋ ๊ฒ์ ๋ณด์ค ์ ์์ต๋๋ค.์ด๋ ๊ฒ ์์ฑํ ๊ฐ์ฒด๋ ๋ฐํํ์ฌ BeanDeserializer๋ก ๋์ง๋๋ค.
(2) p.currentName()์ ํตํด์ json์์ ํ๋กํผํฐ ์ด๋ฆ์ ํ์ฑํฉ๋๋ค.
(3) ์๋ find ๋ฉ์๋๋ฅผ ํตํด์ ํ๋กํผํฐ ์ด๋ฆ์ ๊ฐ์ ธ์ต๋๋ค.์๋ ํ๋กํผํฐ๊ฐ ์๋ ๊ฒฝ์ฐ find ๋ฉ์๋์์ null์ ๋ฐํํ์ฌ Exception์ด ํฐ์ง๋ ๊ฒ ๊ฐ์ต๋๋ค. (์ ํํ๊ฒ๋ ํ์ธํ์ง ๋ชปํ์ต๋๋ค.. ์์๋๋ถ์ ๊ผญ ๋๊ธ ๋ถํ๋๋ฆฝ๋๋ค..)
public SettableBeanProperty find(String key)
{
if (key == null) {
throw new IllegalArgumentException("Cannot pass null property name");
}
if (_caseInsensitive) {
key = key.toLowerCase(_locale);
}
// inlined `_hashCode(key)`
int slot = key.hashCode() & _hashMask;
// int h = key.hashCode();
// int slot = (h + (h >> 13)) & _hashMask;
int ix = (slot<<1);
Object match = _hashArea[ix];
if ((match == key) || key.equals(match)) {
return (SettableBeanProperty) _hashArea[ix+1];
}
return _find2(key, slot, match);
}
(4) ํด๋น ํ๋กํผํฐ์ ๊ฐ์ ์ญ์ง๋ ฌํ๋ฅผ ํ์ฌ ๊ฐ์ ์ค์ ํฉ๋๋ค.
@Override
public void deserializeAndSet(JsonParser p,
DeserializationContext ctxt, Object instance) throws IOException
{
Object value;
if (p.hasToken(JsonToken.VALUE_NULL)) {
if (_skipNulls) {
return;
}
value = _nullProvider.getNullValue(ctxt);
} else if (_valueTypeDeserializer == null) {
value = _valueDeserializer.deserialize(p, ctxt); // (1)
// 04-May-2018, tatu: [databind#2023] Coercion from String (mostly) can give null
if (value == null) {
if (_skipNulls) {
return;
}
value = _nullProvider.getNullValue(ctxt);
}
} else {
value = _valueDeserializer.deserializeWithType(p, ctxt, _valueTypeDeserializer);
}
try {
_field.set(instance, value); // (2)
} catch (Exception e) {
_throwAsIOE(p, e, value);
}
}
(1) ์ญ์ง๋ ฌํ๋ฅผ ํตํด ํด๋น ํ๋กํผํฐ์ ๊ฐ์ ๊ฐ์ ธ์ต๋๋ค.
(2) ์ด์ ์ ์์ฑํ Test ์ธ์คํด์ค์ ํด๋น ๊ฐ์ ์ ์ฅํฉ๋๋ค.
์ฌ๊ธฐ์ _field๋ฅผ ์ฐ์ด๋ณด๋ฉด ํด๋น ํ๋์ ์ง์ ์ฃผ์ ์ ํ๊ณ ์์ต๋๋ค.
(5) ๋ค์ ํ๋กํผํฐ๊ฐ ์์ ๋๊น์ง ์ด๋ฅผ ๋ฐ๋ณตํฉ๋๋ค.
์ ์ด์ ๋ชจ๋ ์๋ฌธ์ด ํ๋ ธ์ต๋๋ค.
How Jackson ObjectMapper Matches JSON Fields to Java Fields
To read Java objects from JSON with Jackson properly, it is important to know how Jackson maps the fields of a JSON object to the fields of a Java object, so I will explain how Jackson does that.
By default Jackson maps the fields of a JSON object to fields in a Java object by matching the names of the JSON field to the getter and setter methods in the Java object. Jackson removes the "get" and "set" part of the names of the getter and setter methods, and converts the first character of the remaining name to lowercase.
For instance, the JSON field named
brand
matches the Java getter and setter methods calledgetBrand()
andsetBrand()
. The JSON field namedengineNumber
would match the getter and setter namedgetEngineNumber()
andsetEngineNumber()
.If you need to match JSON object fields to Java object fields in a different way, you need to either use a custom serializer and deserializer, or use some of the many Jackson Annotations.
JSON์ ํ๋๊ฐ์ Java ๊ฐ์ฒด์ ํ๋๊ฐ์ด๋ ๋งคํ ์ํฌ ๋ Java ๊ฐ์ฒด์ ํ๋๊ฐ์ ์ฐพ๋๊ฒ ์๋๊ณ Getter, Setter ๋ฉ์๋ ๋ช
์์ get
, set
๋ฅผ ์ ์ธํ ๋๋จธ์ง ๋ฌธ์์ด์ ์๋ฌธ์๋ก ๋ณ๊ฒฝํ ๋ค ๋งค์นญ ์ํจ๋ค๋ ๊ฒ์
๋๋ค..!
์ง์ง์ธ์ง ํ์ธํด๋ณผ๊น์?
@NoArgsConstructor
@AllArgsConstructor
public class People {
private String name;
private int age;
private String city;
public String getName() {
return name;
}
public int getAge() {
return age;
}
// city์ getter๊ฐ ์์!
}
city์ getter๋ฅผ ์ ๊ฑฐํด๋ดค์ต๋๋ค.
๊ทธ๋ฆฌ๊ณ ์๋ ํ ์คํธ๋ฅผ ๋๋ ค๋ณด๋ฉด.?
@Test
void objectMapper_test() throws IOException {
String json = "{ \"name\" : \"shlee\", \"age\" : 5, \"city\" : \"city\"}";
Reader reader = new StringReader(json);
ObjectMapper objectMapper = new ObjectMapper();
People people = objectMapper.readValue(reader, People.class);
}
๋๋ฅ..! city๋ฅผ ์ฐพ์ ์ ์๋ค๊ณ ํฉ๋๋ค..!
Unrecognized field "city" (class com.example.testrestdocs.entity.People), not marked as ignorable (2 known properties: "name", "age"])
at [Source: (StringReader); line: 1, column: 42] (through reference chain: com.example.testrestdocs.entity.People["city"])
com.fasterxml.jackson.databind.exc.UnrecognizedPropertyException: Unrecognized field "city" (class com.example.testrestdocs.entity.People), not marked as ignorable (2 known properties: "name", "age"])
at [Source: (StringReader); line: 1, column: 42] (through reference chain: com.example.testrestdocs.entity.People["city"])
- city์ getter ๋๋ setter๋ฅผ ์ถ๊ฐํ๋ฉด ํ ์คํธ๊ฐ ํต๊ณผํ๊ฒ ๋ฉ๋๋ค.