Reputation: 369
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
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.
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);
}
}
}
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()
: switch
es (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());
}
}
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);
}
}
}
}
The following interfaces just have their unchecked exception counterparts in JDK 8 and can be enhanced if necessary.
interface IConsumer<T> {
void accept(T t)
throws IOException;
}
interface ISupplier<T> {
T get()
throws IOException;
}
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
Reputation: 15307
You can use Jackson JsonPropertyOrder
in your Java class.
@JsonPropertyOrder({ "firstName", "lastName", "age" })
public class MyClass { ... }
Upvotes: 2