David A David A - 1 month ago 16
Java Question

Jackson stops reading input string after first closing curly bracket

I have some code that takes an array of bytes. Those bytes, when converted to a String, should be a valid JSON String. If it is not, it will convert the String to valid JSON using "Uknown" as its key.

It works fine except for one edge case I have found. If I pass it a String that has more than one valid JSON string in it, it only parses the first String and considers it to be valid JSON. I would rather it evaluate the entire String and determine that it is not valid JSON since it is 2 or more separate valid JSON Strings. Then, it would make the separate JSON Strings into one valid JSON String as it does for any other String that is not valid JSON.

I am using Jackson 2.8.1.

Below is a small application that demonstrates the problem. Any help would be appreciated.

import java.io.IOException;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.util.Iterator;

import com.fasterxml.jackson.core.JsonFactory;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.JsonNodeFactory;
import com.fasterxml.jackson.databind.node.ObjectNode;

public class EnsureValidJSON {

private static ObjectMapper objectMapper = new ObjectMapper();

public static void main(String[] args) {

String input = "{\"Message1\" : \"This is the first message\"}{\"Message2\" : \"This is the second message.\"}";
System.out.println("input: " + input);

byte[] msg = input.getBytes();
try {
msg = ensureMsgIsValidJson(msg);
} catch (IOException e) {
// Default to Unknown:Unknown
msg = "{\"Unknown\" : \"Unknown\"}".getBytes();
}

System.out.println("output: " + new String(msg));
}

private static boolean isJSONValid(byte[] msg) {
boolean isValid = false;
try {
JsonNode jsonNode = objectMapper.readTree(msg);

// Print out the field names and their values to show that it is only parsing the first Json String.
Iterator<String> itr = jsonNode.fieldNames();
while (itr.hasNext()) {
String fieldName = itr.next();
System.out.print(fieldName + ": ");
System.out.println(jsonNode.get(fieldName));
}
isValid = true;
} catch (IOException e) {
String err = String.format("%s is an invalid JSON message. We will attempt to make the message valid JSON. Its key will be 'Unknown'.", new String(msg));
System.out.println(err);
}

return isValid;
}

private static byte[] ensureMsgIsValidJson(byte[] msg) throws IOException {
if (isJSONValid(msg)) {
return msg;
}
return createValidJSON(msg);

}

private static byte[] createValidJSON(byte[] msg) throws IOException {
JsonFactory factory = new JsonFactory();
try (OutputStream out = new ByteArrayOutputStream()) {
JsonGenerator generator = factory.createGenerator(out);
generator.writeBinary(msg);

JsonNodeFactory nodeFactory = new JsonNodeFactory(false);
ObjectNode validated = nodeFactory.objectNode();
objectMapper.writeTree(generator, validated);
validated.put("Unknown", new String(msg));
byte[] validatedBytes = objectMapper.writeValueAsBytes(validated);
String message = String.format("Message(%s) was successfully converted to a valid JSON message: %s", new String(msg), new String(validatedBytes));
System.out.println(message);
return validatedBytes;
}
}

}

Answer

I had to use a jackson JsonParser object to count the number of opening and closing curly brackets. If the count is 0 and there is nothing left in the String, it has only one JSON String in it. I also had to add code to check if the value is numeric because ObjectMapper's readTree method will not throw an IOException if the value evaluates to a number.

It's more code I was wanting to write to get this done, but it works:

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.util.Scanner;

import com.fasterxml.jackson.core.JsonFactory;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonToken;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.JsonNodeFactory;
import com.fasterxml.jackson.databind.node.ObjectNode;

public class EnsureValidJSON {

  private static ObjectMapper objectMapper = new ObjectMapper();

  public static void main(String[] args) {
    if (args.length == 0) {
      System.err.println("You must pass at least one String to be validated.");
    } else {

      for (String arg : args) {
        System.out.println("input: " + arg);
        byte[] msg = arg.getBytes();
        try {
          msg = ensureMsgIsValidJson(msg);
        } catch (IOException e) {
          msg = "{\"Unknown\" : \"Unknown\"}".getBytes();
        }
        System.out.println("output: " + new String(msg));
      }
    }
  }

  private static boolean isJSONValid(byte[] msg) {
    return isJSONFormat(msg) && isJSONOneMessage(msg);
  }

  private static boolean isJSONFormat(byte[] msg) {
    boolean isValid = false;
    String rawString = new String(msg).trim();
    try (Scanner sc = new Scanner(rawString)) {
      objectMapper.readTree(msg);
      // If the value evaluates to a number, objectMapper.readTree will not throw an Exception, so check that here.
      if (sc.hasNextLong() || sc.hasNextDouble()) {
        String err = String.format("%s is an invalid JSON message because it is numeric.", rawString);
        System.out.println(err);
      } else {
        isValid = true;
      }
    } catch (IOException e) {
      String err = String.format("%s is an invalid JSON message. We will attempt to make the message valid JSON. Its key will be 'Unknown'.", rawString);
      System.out.println(err);
    }

    return isValid;
  }

  private static boolean isJSONOneMessage(byte[] msg) {
    boolean isValid = false;
    try {
      JsonParser parser = objectMapper.getFactory().createParser(msg);
      JsonToken token;
      // balance will increment with each opening curly bracket and decrement with each closing curly bracket. We'll use this to ensure that this is only one JSON message.
      int balance = 0;
      while ((token = parser.nextToken()) != null) {
        if (token.isStructStart()) {
          balance++;
        } else if (token.isStructEnd()) {
          balance--;
        }
        if (balance == 0) {
          break;
        }
      }
      isValid = parser.nextToken() == null;
    } catch (IOException e) {
      String err = String.format("'%s' is an invalid JSON message due to the following error: '%s'. We will attempt to make the message valid JSON. Its key will be 'Unknown'.", new String(msg),
          e.getMessage());
      System.out.println(err);
    }

    return isValid;
  }

  private static byte[] ensureMsgIsValidJson(byte[] msg) throws IOException {
    return isJSONValid(msg) ? msg : createValidJSON(msg);
  }

  private static byte[] createValidJSON(byte[] msg) throws IOException {
    JsonFactory factory = new JsonFactory();
    try (OutputStream out = new ByteArrayOutputStream()) {
      JsonGenerator generator = factory.createGenerator(out);
      generator.writeBinary(msg);

      JsonNodeFactory nodeFactory = new JsonNodeFactory(false);
      ObjectNode validated = nodeFactory.objectNode();
      objectMapper.writeTree(generator, validated);
      validated.put("Unknown", new String(msg));
      byte[] validatedBytes = objectMapper.writeValueAsBytes(validated);
      String message = String.format("Message(%s) was successfully converted to a valid JSON message: %s", new String(msg), new String(validatedBytes));
      System.out.println(message);
      return validatedBytes;
    }
  }

}