Sending/Parsing multiple JSON objects
I have a Sinatra server that is returning multiple JSON objects from the database in a streaming manner. The objects would look like:
{"a": 1, "b": 2, "c": 3}
{"a": 4, "b": 5, "c": 6}
...
but this is invalid JSON. I can add a hack into Sinatra's event processing (manually injecting the missing array delimiters) to make the response look like:
[
{"a": 1, "b": 2, "c": 3}
, {"a": 4, "b": 5, "c": 6}
]
which is valid JSON now, but this technique is inelegant. Is there some way to do this client-side? Basically, what I want is to have a JavaScript f开发者_开发百科unction read a string and consume a valid JSON object, and then return to me the JSON object and the remainder of the string, iteratively being called until the entire string is consumed.
The native JSON.parse()
function expect the whole string to be valid JSON. I'm not aware of a parser that only consumes the first valid object as you'd like. And people should really be producing valid JSON anyways.
If you know that there is one object per line you could simply split the string by line using the split()
function and parse each line individually.
var str = '{"a": 1, "b": 2, "c": 3}\n'+
'{"a": 4, "b": 5, "c": 6}';
var strLines = str.split("\n");
for (var i in strLines) {
var obj = JSON.parse(strLines[i]);
console.log(obj.a);
}
You could also use a bit of string manipulation to transform each line into an array element and parse the whole thing.
str = "["+str.replace(/\n/g, ",")+"]";
JSON.parse(str);
I would do this:
var str = '{"a": 1, "b": 2, "c": 3}{"a": 4, "b": 5, "c": 6}';
var res = JSON.parse('[' + str.replace(/}{/g, '},{') + ']');
Edit:
as awnser on tremby's comment
var str = '{"a": 1, "b": 2, "c": 3}{"a": 4, "b": 5, "c": 6}';
var res = JSON.parse('[' + str.replace(/}{(?=([^"]*"[^"]*")*[^"]*$)/g, '},{') + ']');
I wrote a small module today to do this and published it on NPM as json-multi-parse
. The code is available on Github.
My solution is simple, but admittedly possibly brittle since it relies on the error message JSON.parse
throws when parsing such a string. It uses the position number given in the error (the number in "Unexpected token { in JSON at position xyz") to parse everything up to before that, then recurse and parse everything after.
However, it won't break due to curly braces in strings as some of the other suggestion solutions here will.
Here's the simple version of the code, which will work in Chrome and Node.
const ERROR_REGEX = /^Unexpected token { in JSON at position (\d+)$/;
function jsonMultiParse(input, acc = []) {
if (input.trim().length === 0) {
return acc;
}
try {
acc.push(JSON.parse(input));
return acc;
} catch (error) {
const match = error.message.match(ERROR_REGEX);
if (!match) {
throw error;
}
const index = parseInt(match[1], 10);
acc.push(JSON.parse(input.substr(0, index)));
return jsonMultiParse(input.substr(index), acc);
}
}
It gets more complicated if you want to support Firefox too, which gives its error in a format giving line number and character within that line. The module I linked above handles this case.
If the JSON strings are single line you can do something like this:
var splitPoint = remainingData.indexOf("\n");
var currentJSONStr = splitPoint > -1 ? remainingData.substr(0, splitPoint) : remainingData;
remainingData = splitPoint > -1 ? remainingData.substr(splitPoint+1) : '';
var dataObj = youJSONDecodeFuncOrEval(currentJSONStr);
If not, just ignore my answer.
I hope this helps you,
Alin
Note: I tried to fulfill the requirement
Basically, what I want is to have a JavaScript function read a string and consume a valid JSON object, and then return to me the JSON object and the remainder of the string, iteratively being called until the entire string is consumed.
this is why I didn't use .split("\n")
.
This may not be the most efficient, but should get the job done.
var s = '{"a": 1, "b": 2, "c": 3}{"a": 4, "b": 5, "c": 6}';
var sTemp = "";
var aObjs = [];
for(var i=0; i<s.length; ++i)
{
sTemp += s[i];
if (s[i] == "}")
{
aObjs.push(JSON.parse(sTemp));
sTemp = "";
}
}
If you know there is a new line character in between objects, it becomes much simpler.
var sBad = '{"a": 1, "b": 2, "c": 3}\n{"a": 4, "b": 5, "c": 6}';
var sGood = "[" + sBad.replace(/\n/g, ",") + "]";
var aObjs = JSON.parse(sGood);
o.string is json Object.
add some string like "new" to array of objects or multiple json objects.
for eg:
json object----
{"id":2,"method":"listWirings","params":{"language":"anonymousLanguage","name":"mytest","working":"{\"modules\":[{\"config\":{\"position\":[186,59],\"xtype\":\"WireIt.ImageContainer\"},\"name\":\"Start\",\"value\":{}},{\"config\":{\"position\":[188,265],\"xtype\":\"WireIt.ImageContainer\"},\"name\":\"Stop\",\"value\":{}}],\"properties\":{\"description\":\"gfd\",\"name\":\"gf\"},\"wires\":[{\"src\":{\"moduleId\":0,\"terminal\":\"_OUTPUT\"},\"tgt\":{\"moduleId\":1,\"terminal\":\"StpIn\"}}]}"},"version":"json-rpc-2.0"}new
var str = o.toString();
var s = str.split("new");
for (var i = 0; i < s.length-1; i++)
{
var r = YAHOO.lang.JSON.parse(s[i]);
}
hope this will to parse multiple json objects .
I wrote this little JavaScript function, which allows you to parse any string to Json objects. It works by passing through each character and taking note of the hierarchy. The benefit of this Solution is, that you can get all the Json objects of a text without knowing what is separating them.
function evaluateJsonString(string){
var start = string.indexOf('{');
if(start == -1)
return false;
let hierarchy = 0;
let characters = string.split('');
let objects = [];
for(var index = start; index < characters.length; index++){
let char = characters[index];
if(char == '{')
hierarchy++;
if(char == '}')
hierarchy--;
if(hierarchy == 0){
objects.push(JSON.parse(characters.slice(start, index + 1).join('')));
index = start = index + characters.slice(index, characters.length).indexOf('{') - 1;
if(start == -1)
break;
}
}
return objects;
}
let result = evaluateJsonString('This is {"name": "John", "age": 32, "hobbies": ["sport", "programming"]} He goes to {"name": "University", "director": {"name": "Peter", "age": 66, "hobbies": ["drinking coffee"]}}');
console.log(result);
If the data stream is giving you multiple JSON objects in one line, you need to separate them into an array:
const str = '{"a": 1, "b": 2, "c": 3}\n' +
'{"a": 4, "b": 5, "c": 6}' +
'{"a": 7, "b": 8, "c": 9}';
const json = '[' + str.replace(/}\n?{/g, '},{') + ']';
JSON.parse(json).forEach((obj) => {
console.log('a:', obj.a);
});
In my case I had to add an edge case just in case if it has one JSON object in TypeScript.
const str = '{"a": 1, "b": 2, "c": 3}\n';
const strLines: string[] = str.split("\n");
for (let i=0; i<strLines.length; i++) {
if (strLines[i] === '') {
break
}
const obj = JSON.parse(strLines[i])
console.log(`obj.a: ${obj.a}`); // 1
}
I wrote a java converter (using the jackson library) that turns multiple JSON objects in a file into the proper JSON array:
import java.io.File;
import com.fasterxml.jackson.core.JsonFactory;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonToken;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.MappingJsonFactory;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.JsonNodeFactory;
import com.fasterxml.jackson.databind.node.ObjectNode;
public class ParseJson {
ObjectMapper mapper = new ObjectMapper();
public static void main(String[] args) throws Exception {
File file = new File(args[0]);
JsonNode jn = new Parser().parse(file);
System.out.println(jn.toString());
}
private enum ParserState {
start,
object,
array,
field,
done
};
private static class Parser {
public Parser() {
}
public JsonNode parse(File file) throws Exception {
JsonNodeFactory factory = JsonNodeFactory.instance;
JsonFactory mappingFactory = new MappingJsonFactory();
@SuppressWarnings("deprecation")
JsonParser jp = mappingFactory.createJsonParser(file);
int n = 0;
JsonNode result = null;
JsonNode jn;
while((jn = parseNode(jp, false)) != null) {
if(n == 0) {
result = jn;
} else if(n == 1) {
ArrayNode an = factory.arrayNode();
an.add(result);
an.add(jn);
result = an;
} else if(n > 1) {
ArrayNode an = (ArrayNode)result;
an.add(jn);
} else {
throw new Exception("Unexpected parser state");
}
n++;
}
return result;
}
private JsonNode parseNode(JsonParser jp, boolean current) throws Exception {
JsonNodeFactory factory = JsonNodeFactory.instance;
ParserState state = ParserState.start;
JsonNode result = null;
String fieldName = null;
JsonToken token = current ? jp.getCurrentToken() : jp.nextToken();
for(; token != null; token = jp.nextToken()) {
// System.out.println("Token: "+token+": "+jp.getValueAsString());
switch(token) {
/**
* NOT_AVAILABLE can be returned if {@link JsonParser}
* implementation can not currently return the requested
* token (usually next one), or even if any will be
* available, but that may be able to determine this in
* future. This is the case with non-blocking parsers --
* they can not block to wait for more data to parse and
* must return something.
*/
case NOT_AVAILABLE: {
break;
}
/**
* START_OBJECT is returned when encountering '{'
* which signals starting of an Object value.
*/
case START_OBJECT: {
switch(state) {
case start: {
assert result == null;
assert fieldName == null;
result = factory.objectNode();
state = ParserState.object;
break;
}
case field: {
assert result != null;
assert fieldName != null;
ObjectNode on = (ObjectNode)result;
JsonNode jn = parseNode(jp, true);
on.set(fieldName, jn);
fieldName = null;
state = ParserState.object;
break;
}
case array: {
assert result != null;
assert fieldName == null;
ArrayNode an = (ArrayNode)result;
JsonNode jn = parseNode(jp, true);
an.add(jn);
break;
}
default: {
throw new Exception("Unexpected state: "+state+", for token: "+token);
}
}
break;
}
/**
* END_OBJECT is returned when encountering '}'
* which signals ending of an Object value
*/
case END_OBJECT: {
switch(state) {
case object: {
assert result != null;
assert fieldName == null;
state = ParserState.done;
break;
}
default: {
throw new Exception("Unexpected state: "+state+", for token: "+token);
}
}
break;
}
/**
* START_ARRAY is returned when encountering '['
* which signals starting of an Array value
*/
case START_ARRAY: {
switch(state) {
case start: {
assert result == null;
assert fieldName == null;
result = factory.arrayNode();
state = ParserState.array;
break;
}
case field: {
assert result != null;
assert fieldName != null;
ObjectNode on = (ObjectNode)result;
JsonNode jn = parseNode(jp, true);
on.set(fieldName, jn);
fieldName = null;
state = ParserState.object;
break;
}
case array: {
assert result != null;
assert fieldName == null;
ArrayNode an = (ArrayNode)result;
JsonNode jn = parseNode(jp, true);
an.add(jn);
break;
}
default: {
throw new Exception("Unexpected state: "+state+", for token: "+token);
}
}
break;
}
/**
* END_ARRAY is returned when encountering ']'
* which signals ending of an Array value
*/
case END_ARRAY: {
switch(state) {
case array: {
assert result != null;
assert fieldName == null;
state = ParserState.done;
break;
}
default: {
throw new Exception("Unexpected state: "+state+", for token: "+token);
}
}
break;
}
/**
* FIELD_NAME is returned when a String token is encountered
* as a field name (same lexical value, different function)
*/
case FIELD_NAME: {
fieldName = jp.getValueAsString();
switch(state) {
case object: {
assert result != null;
assert fieldName == null;
state = ParserState.field;
break;
}
default: {
throw new Exception("Unexpected state: "+state+", for token: "+token);
}
}
break;
}
/**
* Placeholder token returned when the input source has a concept
* of embedded Object that are not accessible as usual structure
* (of starting with {@link #START_OBJECT}, having values, ending with
* {@link #END_OBJECT}), but as "raw" objects.
*<p>
* Note: this token is never returned by regular JSON readers, but
* only by readers that expose other kinds of source (like
* <code>JsonNode</code>-based JSON trees, Maps, Lists and such).
*/
case VALUE_EMBEDDED_OBJECT: {
throw new Exception("Token not supported: "+token);
}
/**
* VALUE_STRING is returned when a String token is encountered
* in value context (array element, field value, or root-level
* stand-alone value)
*/
case VALUE_STRING: {
switch(state) {
case start: {
assert result == null;
assert fieldName == null;
result = factory.textNode(jp.getValueAsString());
state = ParserState.done;
break;
}
case field: {
assert result != null;
assert fieldName != null;
ObjectNode on = (ObjectNode)result;
JsonNode jn = factory.textNode(jp.getValueAsString());
on.set(fieldName, jn);
fieldName = null;
state = ParserState.object;
break;
}
case array: {
assert result != null;
assert fieldName == null;
ArrayNode an = (ArrayNode)result;
JsonNode jn = factory.textNode(jp.getValueAsString());
an.add(jn);
break;
}
default: {
throw new Exception("Unexpected state: "+state+", for token: "+token);
}
}
break;
}
/**
* VALUE_NUMBER_INT is returned when an integer numeric token is
* encountered in value context: that is, a number that does
* not have floating point or exponent marker in it (consists
* only of an optional sign, followed by one or more digits)
*/
case VALUE_NUMBER_INT: {
switch(state) {
case start: {
assert result == null;
assert fieldName == null;
result = factory.numberNode(jp.getLongValue());
state = ParserState.done;
break;
}
case field: {
assert result != null;
assert fieldName != null;
ObjectNode on = (ObjectNode)result;
JsonNode jn = factory.numberNode(jp.getLongValue());
on.set(fieldName, jn);
fieldName = null;
state = ParserState.object;
break;
}
case array: {
assert result != null;
assert fieldName == null;
ArrayNode an = (ArrayNode)result;
JsonNode jn = factory.numberNode(jp.getLongValue());
an.add(jn);
break;
}
default: {
throw new Exception("Unexpected state: "+state+", for token: "+token);
}
}
break;
}
/**
* VALUE_NUMBER_INT is returned when a numeric token other
* that is not an integer is encountered: that is, a number that does
* have floating point or exponent marker in it, in addition
* to one or more digits.
*/
case VALUE_NUMBER_FLOAT: {
switch(state) {
case start: {
assert result == null;
assert fieldName == null;
result = factory.numberNode(jp.getDoubleValue());
state = ParserState.done;
break;
}
case field: {
assert result != null;
assert fieldName != null;
ObjectNode on = (ObjectNode)result;
JsonNode jn = factory.numberNode(jp.getDoubleValue());
on.set(fieldName, jn);
fieldName = null;
state = ParserState.object;
break;
}
case array: {
assert result != null;
assert fieldName == null;
ArrayNode an = (ArrayNode)result;
JsonNode jn = factory.numberNode(jp.getDoubleValue());
an.add(jn);
break;
}
default: {
throw new Exception("Unexpected state: "+state+", for token: "+token);
}
}
break;
}
/**
* VALUE_TRUE is returned when encountering literal "true" in
* value context
*/
case VALUE_TRUE: {
switch(state) {
case start: {
assert result == null;
assert fieldName == null;
result = factory.booleanNode(true);
state = ParserState.done;
break;
}
case field: {
assert result != null;
assert fieldName != null;
ObjectNode on = (ObjectNode)result;
JsonNode jn = factory.booleanNode(true);
on.set(fieldName, jn);
fieldName = null;
state = ParserState.object;
break;
}
case array: {
assert result != null;
assert fieldName == null;
ArrayNode an = (ArrayNode)result;
JsonNode jn = factory.booleanNode(true);
an.add(jn);
break;
}
default: {
throw new Exception("Unexpected state: "+state+", for token: "+token);
}
}
break;
}
/**
* VALUE_FALSE is returned when encountering literal "false" in
* value context
*/
case VALUE_FALSE: {
switch(state) {
case start: {
assert result == null;
assert fieldName == null;
result = factory.booleanNode(false);
state = ParserState.done;
break;
}
case field: {
assert result != null;
assert fieldName != null;
ObjectNode on = (ObjectNode)result;
JsonNode jn = factory.booleanNode(false);
on.set(fieldName, jn);
fieldName = null;
state = ParserState.object;
break;
}
case array: {
assert result != null;
assert fieldName == null;
ArrayNode an = (ArrayNode)result;
JsonNode jn = factory.booleanNode(false);
an.add(jn);
break;
}
default: {
throw new Exception("Unexpected state: "+state+", for token: "+token);
}
}
break;
}
/**
* VALUE_NULL is returned when encountering literal "null" in
* value context
*/
case VALUE_NULL: {
switch(state) {
case start: {
assert result == null;
assert fieldName == null;
result = factory.nullNode();
state = ParserState.done;
break;
}
case field: {
assert result != null;
assert fieldName != null;
ObjectNode on = (ObjectNode)result;
JsonNode jn = factory.nullNode();
on.set(fieldName, jn);
fieldName = null;
state = ParserState.object;
break;
}
case array: {
assert result != null;
assert fieldName == null;
ArrayNode an = (ArrayNode)result;
JsonNode jn = factory.nullNode();
an.add(jn);
break;
}
default: {
throw new Exception("Unexpected state: "+state+", for token: "+token);
}
}
break;
}
default: {
throw new Exception("Token not supported: "+token);
}
}
if(state == ParserState.done) {
break;
}
}
return result;
}
}
}
精彩评论