XStream은 Java Object에 대한 xml serialize를 제공하는 Libraray입니다. 물론 deserialize도 지원하며, json 변환 역시 지원하지요.
문제상황
하지만 XStream은 Object의 property 값이 null 인 경우에는 해당 property를 serialize 하지 않습니다. 예를 들면…
@XStreamAlias("Person")
class Person {
public String firstName;
public String lastName;
public String middleName;
}
......
Person jobs = new Person();
jobs.firstName = "Steve";
jobs.lastName = "Jobs";
XStream xstream = new XStream();
xstream.processAnnotations(Person.class);
String xml = xstream.toXML(jobs);
System.out.println(xml);
위와 같은 코드는 아래와 같은 XML을 출력하게 됩니다.
Steve Jobs
해결방법(XML) : Converter 구현
null value serialize를 위해서 여러가지 접근 방법이 있겠지만, Converter를 구현해서 사용하는게 가장 심플합니다.
아래코드는 null value를 빈 element로 변환하도록 하는 Converter 구현입니다.
/**
* NullConverterHack.
*
*
* {@link XStream}으로 serialize 할때 기본적으로 null value를 skip처리하므로,
* 빈 엘리먼트를 삽입하도록 하는 {@link Converter} 클래스임.
*
*
* 아래와 같은 형태로 사용한다. 반드시 LOW PRIORITY로 사용해야 함.
*
* xstream.registerConverter(new NullConverterHack(xstream.getMapper()), XStream.PRIORITY_LOW);
*
*
*
* (주의) unmarshal을 지원하지 않는다.
*
* @see JsonWriterHack Json으로 serialize 할 경우에는 {@link NullConverterHack}과 {@link JsonWriterHack}을 함께 사용한다.
* @author oddpoet
*/
public class NullConverterHack implements Converter {
private Mapper mapper;
public NullConverterHack(Mapper mapper) {
this.mapper = mapper;
}
/**
* 모든 클래스에 대해서 작동.
*
* @param type
* @return
* @see com.thoughtworks.xstream.converters.ConverterMatcher#canConvert(java.lang.Class)
*/
@SuppressWarnings("unchecked")
public boolean canConvert(Class type) {
return true;
}
/**
* marshalling.
*
* @param source
* @param writer
* @param context
* @see com.thoughtworks.xstream.converters.Converter#marshal(java.lang.Object, com.thoughtworks.xstream.io.HierarchicalStreamWriter, com.thoughtworks.xstream.converters.MarshallingContext)
*/
public void marshal(Object source, HierarchicalStreamWriter writer, MarshallingContext context) {
Field [] fieldSet = source.getClass().getDeclaredFields();
for (Field field : fieldSet) {
Object member;
if (!mapper.shouldSerializeMember(source.getClass(), field.getName())) {
continue;
}
field.setAccessible(true);
try {
member = field.get(source);
String name = mapper.serializedMember(field.getDeclaringClass(), field.getName());
if (member == null) {
writer.startNode(name);
// 값이 null이면 내용은 채우지 않고 startNode(), endNode()만 호출
writer.endNode();
} else {
ExtendedHierarchicalStreamWriterHelper.startNode(writer, name, member.getClass());
context.convertAnother(member);
writer.endNode();
}
} catch (IllegalArgumentException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
}
/**
* unmarshal을 지원하지 않는다.
*
* @param reader
* @param context
* @return
* @see com.thoughtworks.xstream.converters.Converter#unmarshal(com.thoughtworks.xstream.io.HierarchicalStreamReader, com.thoughtworks.xstream.converters.UnmarshallingContext)
*/
public Object unmarshal(HierarchicalStreamReader reader, UnmarshallingContext context) {
return null;
}
}
Converter 구현 코드가 길지 않으니 자세한 설명은 생략하겠습니다. 실제 사용방법은 아래와 같습니다.
Person jobs = new Person(); jobs.firstName = "Steve"; jobs.lastName = ""; jobs.middleName = null; XStream xstream = new XStream(); // Converter를 등록한다. 단, PRIORITY_LOW로 등록하도록 한다. xstream.registerConverter(new NullConverterHack(xstream.getMapper()), XStream.PRIORITY_LOW); xstream.processAnnotations(Person.class); String xml = xstream.toXML(jobs); System.out.println(xml);
위와 같은 방법으로 객체를 XML로 serialize 하면 아래와 같은 결과를 얻을 수 있습니다. 공백문자와 null이 다른 형식으로 표현되고 있음에 유의하세요.
Steve
JSON의 경우
위와 같이 Converter를 추가해서 사용할 경우 XML 변환시 null 값에 대한 빈 element 출력은 가능합니다만, JSON 출력의 경우 아래와 같이 null 이 아닌 빈 객체({})로 출력되는 문제가 있습니다.
Person jobs = new Person();
jobs.firstName = "Steve";
jobs.lastName = "";
jobs.middleName = null;
XStream xstream = new XStream(new JsonHierarchicalStreamDriver() {
@Override
public HierarchicalStreamWriter createWriter(Writer out) {
return new JsonWriter(out, JsonWriter.DROP_ROOT_MODE | JsonWriter.STRICT_MODE);
}
});
xstream.registerConverter(new NullConverterHack(xstream.getMapper()), XStream.PRIORITY_LOW);
xstream.processAnnotations(Person.class);
String xml = xstream.toXML(jobs);
System.out.println(xml);
즉 위와 같은 코드는 아래와 같은 JSON을 출력하게됩니다. null 대신 {}이 출력되지요.
{
"firstName": "Steve",
"lastName": "",
"middleName": {}
}
Conveter의 Interface를 보면 알 수 있겠지만, JSON 출력시의 null 문제를 풀기 위해서는 Conveter 레벨에서 처리가 안됩니다. 결국 Writer 레벨에서 null 처리를 해야합니다. 그런데 기존 JsonWriter를 상속받아서 해결할 수 없기 때문에, 깔끔하지는 않지만 기존 JsonWriter 소스를 복사해서 새로운 클래스를 만들고 관련된 부분을 수정합니다. 수정해야할 부분은 endNode() 함수입니다. (아래 소스 참고)
/**
* JsonWriterHack.
*
*
* XStream 패키지에 있는 {@link JsonWriter}에 수정을 가한 클래스이며, (상속받아서 해결 불가능함)
* {@link NullConverterHack}과 함께 사용할 경우, null 값을 Json의 null로 변환해준다. 당연히 공백문자열("")과는 구분된다.
*
*
* 수정된 부분은 {@link #endNode()} 함수이며, 수정한 부분에 comment를 달아놓았다.
* 하지만 변경된 JsonWriter에 의해 사용시 예기치 못한 JSON 변환이 이루어질 수 있으므로, 주의하도록한다.
*
*
* @see NullConverterHack {@link NullConverterHack}과 함께 사용해야 한다.
* @author oddpoet
*/
public class JsonWriterHack implements ExtendedHierarchicalStreamWriter {
// ... 나머지 코드는 JsonWriter와 동일하게 그대로 두고, endNode() 함수만 수정한다.
public void endNode() {
depth--;
Node node = (Node)elementStack.pop();
if (node.clazz != null && node.isCollection) {
if (node.fieldAlready) {
readyForNewLine = true;
}
finishTag();
writer.write("]");
} else if (tagIsEmpty) {
readyForNewLine = false;
writer.write("null"); // <- 이부분을 수정. 원래는 writer.write("{}"); 였음.
finishTag();
} else {
finishTag();
if (node.fieldAlready) {
writer.write("}");
}
}
readyForNewLine = true;
if (depth == 0 && ((mode & DROP_ROOT_MODE) == 0 || (depth > 0 && !node.isCollection))) {
writer.write("}");
writer.flush();
}
}
// 나머지 코드는 그대로 유지...
}
자 그럼, JsonWriter 대신 위에서 만든 JsonWriterHack으로 대체하면 원하는 null 출력을 볼 수 있습니다.
Person jobs = new Person();
jobs.firstName = "Steve";
jobs.lastName = "";
jobs.middleName = null;
XStream xstream = new XStream(new JsonHierarchicalStreamDriver() {
@Override
public HierarchicalStreamWriter createWriter(Writer out) {
return new JsonWriterHack(out, JsonWriterHack.DROP_ROOT_MODE | JsonWriterHack.STRICT_MODE);
}
});
xstream.registerConverter(new NullConverterHack(xstream.getMapper()), XStream.PRIORITY_LOW);
xstream.processAnnotations(Person.class);
String xml = xstream.toXML(jobs);
System.out.println(xml);
{
"firstName": "Steve",
"lastName": "",
"middleName": null
}
Json에서의 null serialize 의 경우, 기존 구현체를 Rewrite해야해서 그다지 깔끔한 방법은 아닙니다. XStream으로 JSON 출력제어를 하는데 제약이 있으므로, 다른 object – JSON 매핑 library를 사용하는게 좋을 수도 있습니다.
제가 딱 찾던거네요.. 도움 많이 되었습니다.