Search code examples
javaxmljsonxstream

Using XStream and JsonHierarchicalStreamDriver to output values, how do I round doubles?


A similar question is here, and I have indeed tried to use converters, as the answer suggests.

Formatting decimal values for XML

I am serializing objects to both xml and json. I am using XStream, and JsonHierarchicalStreamDriver when needed.

The objects have a properties of type double. These were serializing fine. In xml it serialized as:

<value>3.14159265</value>

In json it was:

value: 3.14159265

I now have a requirement to round the value to a given number of decimal places.

On my first attempt, I wrote a Converter, which rounded all doubles to 2 decimal places. This rendered as above (except the value was rounded), but the problem is that different properties need to be rounded to different significant figures, and the converter has no knowledge how many decimal places when when at the “marshal” method.

My second attempt uses a custom object to replace the double, which holds the double object and the number of decimal places. Another converter is required, and this works well for the xml, but in json it renders the value as a string (i.e. in quotes) not as a number (which is what I really want).

How do I render the rounded double as a number?

Thanks


Solution

  • If anyone is interested, it seems that the JsonHierarchicalStreamDriver uses a class called JsonWriter. Between them, they assume anything that Doubles, Integers etc translate to JSON number, while custom objects will be translated as text (i.e. be wrapped in quotes). There does not seem anyway to specify how you want the value rendered.

    There are several options if you still need it done. You might be able to use an alternative to JsonHierarchicalStreamDriver. The documentation implies that a version of JettisonMappedXmlDriver will turn all number like strings into JSON numbers (not always desired).

    I went with the following dirty hack:

    Create a new writer that allows you to drop quotes:

    public static class HackJsonWriter extends JsonWriter {
        public HackJsonWriter(Writer writer) {
            super(writer);
        }
    
        private boolean dropQuotes = false;
    
        public void dropQuotes() {
            dropQuotes = true;
        }
    
        public void allowQuotes() {
            dropQuotes = false;
        }
    
        @Override
        protected void addValue(String value, AbstractJsonWriter.Type type)
        {
            if (dropQuotes) {
                super.addValue(value, AbstractJsonWriter.Type.NULL);
            }
            else {
                super.addValue(value, type);
            }
        }
    }
    

    Overwrite the creation of the writer in the following way:

    new XStream(new JsonHierarchicalStreamDriver() {
        @Override
        public HierarchicalStreamWriter createWriter(Writer writer) {
            return new HackJsonWriter(writer);
        }
    });
    

    Create a converter that spots the hack, and handles it.

    public static class HackRoundedDoubleConverter implements Converter {
    
        private HackRoundedDoubleConverter() {
        }
    
        @Override
        public boolean canConvert(Class clazz) {
            return RoundedDouble.class.isAssignableFrom(clazz);
        }
    
        @Override
        public void marshal(Object value, HierarchicalStreamWriter writer, MarshallingContext context) {
            if (value == null) {
                return; // now that i think about it, this should probably write "null" (no quotes), but I was handling that elsewhere
            }
            RoundedDouble number = (RoundedDouble)value;
            if (Double.isNaN(number.getValue()) || Double.isInfinite(number.getValue())) {
                return; // at this point, we would have a problem.  an empty node will be created no matter what (and it translates to an empty object in javascript).
            }
    
            if (writer.underlyingWriter() instanceof HackJsonWriter) {
                HackJsonWriter hackWriter = (HackJsonWriter)writer.underlyingWriter();
                hackWriter.dropQuotes();
                writer.setValue(number.getRoundedValue());
                hackWriter.allowQuotes();
            }
            else {
                writer.setValue(number.getRoundedValue());
            }
        }
    
        @Override
        public Object unmarshal(HierarchicalStreamReader reader, UnmarshallingContext context) {
            // only interested in the one direction
            return null;
        }
    }
    

    Register the converter:

        xstream.registerConverter(new HackRoundedDoubleConverter());
    

    RoundedDouble is a class that has two properties, a double value, and a number of decimal places.

    As I say, it is a hack (it is extremely fragile and probably has many problems I haven’t even spotted), and I probably broke the law when working out how to do it. I probably won’t even use it, but it answers my original question.