Pranay
Pranay

Reputation: 369

Manipulating elements inside a JSON: Change the position of elements inside a JSON string

I want to swap the position of elements inside a JSON string. What is the most efficient way of doing it? Do Jackson or Gson provide any functionality to alter the position of elements within a JSON string?

BEFORE:

{
    "firstName": "John",
    "lastName": "Smith",
    "age": 25,
    "address": {
        "streetAddress": "21 2nd Street",
        "city": "New York",
        "state": "NY",
        "postalCode": 10021
    },
    "phoneNumbers": [
        {
            "type": "home",
            "number": "212 555-1234"
        },
        {
            "type": "fax",
            "number": "646 555-4567" 
        }
    ] 
}

AFTER:

{
    "lastName": "Smith",
    "firstName": "John",
    "age": 25,
    "address": {
        "city": "New York",
        "streetAddress": "21 2nd Street",
        "state": "NY",
        "postalCode": 10021
    },
    "phoneNumbers": [
        {
            "type": "home",
            "number": "212 555-1234"
        },
        {
            "type": "fax",
            "number": "646 555-4567" 
        }
    ] 
}

Upvotes: 2

Views: 827

Answers (2)

Lyubomyr Shaydariv
Lyubomyr Shaydariv

Reputation: 21125

What is the most efficient way of doing it?

Usually, the answer is: streaming. The reason of why streaming may be efficient is that you don't have to store the whole value in memory and, in theory, you can process infinite JSON streams.

The following sample is written with Java 8 and Gson, but it's easy to port it to Java 7 and below. It does not accept all possible cases, and since it does not have extra logic, it also swaps $.phoneNumbers[].type and $.phoneNumbers[].number (or does it also matches your request?). Anyway, the code below is an idea of doing that and can be improved on your own.

Swap.java

This utility class provides a method that can read from any reader and write to any writer swapping the first JSON properties (arrays are considered to have elements, not property key/value entries). It's implemented in a FSM fashion, thus it may look bloated.

final class Swap {

    private Swap() {
    }

    static void swapLeadingFields(final Reader from, final Writer to)
            throws IOException {
        final JsonReader reader = new JsonReader(from);
        final JsonWriter writer = new JsonWriter(to);
        final State state = new State();
        while ( reader.peek() != END_DOCUMENT ) {
            final JsonToken token = reader.peek();
            switch ( token ) {
            case BEGIN_ARRAY:
                reader.beginArray();
                state.push(ARRAY);
                writer.beginArray();
                break;
            case END_ARRAY:
                reader.endArray();
                state.pop();
                writer.endArray();
                break;
            case BEGIN_OBJECT:
                reader.beginObject();
                state.push(OBJECT_BEGIN);
                writer.beginObject();
                break;
            case END_OBJECT:
                switch ( state.mode() ) {
                case OBJECT_VALUE_1_FOUND:
                    state.accept(e -> writeProperty(writer, e.key1, e.value1));
                    break;
                case OBJECT_VALUE_2_FOUND:
                    state.accept(e -> {
                        writeProperty(writer, e.key2, e.value2);
                        writeProperty(writer, e.key1, e.value1);
                    });
                    break;
                case OBJECT_BEGIN:
                case OBJECT_SWAPPED:
                    // do nothing
                    break;
                case OBJECT_KEY_1_FOUND:
                case OBJECT_KEY_2_FOUND:
                case ARRAY:
                    throw new IllegalStateException(String.valueOf(state.mode()));
                default:
                    throw new AssertionError(state.mode());
                }
                reader.endObject();
                state.pop();
                writer.endObject();
                break;
            case NAME:
                final String name = reader.nextName();
                switch ( state.mode() ) {
                case OBJECT_BEGIN:
                    state.accept(e -> {
                        e.mode = OBJECT_KEY_1_FOUND;
                        e.key1 = name;
                    });
                    break;
                case OBJECT_VALUE_1_FOUND:
                    state.accept(e -> {
                        e.mode = OBJECT_KEY_2_FOUND;
                        e.key2 = name;
                    });
                    break;
                case OBJECT_VALUE_2_FOUND:
                    state.accept(e -> {
                        e.mode = OBJECT_SWAPPED;
                        writeProperty(writer, e.key2, e.value2);
                        writeProperty(writer, e.key1, e.value1);
                    });
                    writer.name(name);
                    break;
                case OBJECT_SWAPPED:
                    writer.name(name);
                    break;
                case OBJECT_KEY_1_FOUND:
                case OBJECT_KEY_2_FOUND:
                case ARRAY:
                    throw new IllegalStateException(String.valueOf(state.mode()));
                default:
                    throw new AssertionError(state.mode());
                }
                break;
            case STRING:
                handleSimpleValue(state, reader::nextString, writer::value);
                break;
            case NUMBER:
                handleSimpleValue(state, () -> parseBestGsonNumber(reader.nextString()), n -> writeBestNumber(writer, n));
                break;
            case BOOLEAN:
                handleSimpleValue(state, reader::nextBoolean, writer::value);
                break;
            case NULL:
                handleSimpleValue(state, () -> {
                    reader.nextNull();
                    return null;
                }, v -> writer.nullValue());
                break;
            case END_DOCUMENT:
                // do nothing
                break;
            default:
                throw new AssertionError(token);
            }
        }
    }

    private static <T> void handleSimpleValue(final State state, final ISupplier<? extends T> supplier, final IConsumer<? super T> consumer)
            throws IOException {
        final T value = supplier.get();
        switch ( state.mode() ) {
        case OBJECT_KEY_1_FOUND:
            state.accept(e -> {
                e.mode = OBJECT_VALUE_1_FOUND;
                e.value1 = value;
            });
            break;
        case OBJECT_KEY_2_FOUND:
            state.accept(e -> {
                e.mode = OBJECT_VALUE_2_FOUND;
                e.value2 = value;
            });
            break;
        case OBJECT_SWAPPED:
            consumer.accept(value);
            break;
        case OBJECT_BEGIN:
        case OBJECT_VALUE_1_FOUND:
        case OBJECT_VALUE_2_FOUND:
            throw new IllegalStateException(String.valueOf(state.mode()));
        case ARRAY:
        default:
            throw new AssertionError(state.mode());
        }
    }

    private static void writeProperty(final JsonWriter writer, final String key, final Object value)
            throws IOException {
        writer.name(key);
        if ( value instanceof String ) {
            writer.value((String) value);
        } else if ( value instanceof Number ) {
            writeBestNumber(writer, (Number) value);
        } else if ( value instanceof Boolean ) {
            writer.value((boolean) value);
        } else {
            throw new AssertionError(value.getClass());
        }
    }

    private static void writeBestNumber(final JsonWriter writer, final Number number)
            throws IOException {
        if ( number instanceof Double ) {
            writer.value((double) number);
        } else if ( number instanceof Long ) {
            writer.value((long) number);
        } else {
            writer.value(number);
        }
    }

}

State.java

The state class is, in principle, just a data bag with a few state-friendly methods. A note on why it has accept(IConsumer) rather than current(): switches (especially nested ones) do not provide nice scope isolation mechanism for local variables.

final class State {

    enum Mode {

        OBJECT_BEGIN,
        OBJECT_KEY_1_FOUND,
        OBJECT_VALUE_1_FOUND,
        OBJECT_KEY_2_FOUND,
        OBJECT_VALUE_2_FOUND,
        OBJECT_SWAPPED,
        ARRAY

    }

    static final class Element {

        Mode mode;
        String key1;
        Object value1;
        String key2;
        Object value2;

        private Element(final Mode mode) {
            this.mode = mode;
        }

    }

    private final Stack<Element> stack = new Stack<>();

    void push(final Mode mode) {
        stack.push(new Element(mode));
    }

    void pop() {
        stack.pop();
    }

    Mode mode() {
        return stack.peek().mode;
    }

    void accept(final IConsumer<? super Element> consumer)
            throws IOException {
        consumer.accept(stack.peek());
    }

}

Numbers.java

This is just a helper class that tries to detect the "narrowest" data type in the simplest way, however it does not necessarily helps to keep the same literal types from the source and to the destination (say, reader.nextDouble() may lead to 25.0 and 10021.0 as the results).

final class Numbers {

    private Numbers() {
    }

    static Number parseBestGsonNumber(final String rawNumber) {
        try {
            return Integer.parseInt(rawNumber);
        } catch ( final NumberFormatException exParseInt ) {
            try {
                return Long.parseLong(rawNumber);
            } catch ( final NumberFormatException exParseLong ) {
                return Double.parseDouble(rawNumber);
            }
        }
    }

}

A couple of checked exceptions-friendly interfaces

The following interfaces just have their unchecked exception counterparts in JDK 8 and can be enhanced if necessary.

IConsumer.java

interface IConsumer<T> {

    void accept(T t)
            throws IOException;

}

ISupplier.java

interface ISupplier<T> {

    T get()
            throws IOException;

}

EntryPoint.java

And the demo

public final class EntryPoint {

    private EntryPoint() {
    }

    public static void main(final String... args)
            throws IOException {
        try ( InputStream inputStream = currentThread().getContextClassLoader().getResourceAsStream("data.json");
              final Reader from = new InputStreamReader(inputStream) ) {
            final Writer to = new OutputStreamWriter(System.out);
            try {
                swapLeadingFields(from, to);
            } finally {
                to.flush();
            }
        }
    }

}

And the pretty-printed result:

{
    "lastName": "Smith",
    "firstName": "John",
    "age": 25,
    "address": {
        "city": "New York",
        "streetAddress": "21 2nd Street",
        "state": "NY",
        "postalCode": 10021
    },
    "phoneNumbers": [
        {
            "number": "212 555-1234",
            "type": "home"
        },
        {
            "number": "646 555-4567",
            "type": "fax"
        }
    ]
}

Upvotes: 0

abaghel
abaghel

Reputation: 15307

You can use Jackson JsonPropertyOrder in your Java class.

@JsonPropertyOrder({ "firstName", "lastName", "age" })
public class MyClass { ... }

https://fasterxml.github.io/jackson-annotations/javadoc/2.2.0/com/fasterxml/jackson/annotation/JsonPropertyOrder.html

Upvotes: 2

Related Questions