The real answer ("tell between invalid JSON and a string")
For a lot of obvious reasons we're not going to come up with a definite answer - we can at best hope to determine an index that will tell us whether a string might have been JSON.
The best approach I can think of is to use something like the Levenshtein algorithm, with this twist: we have no "B string", but rather we start parsing the candidate string and modify it as soon as we hit an invalid character.
To do this, we can use a JSON streaming parser (state-based). Let's say we parse
{ "key": value" }
then the parser will find the 'v' and choke. At that point we can try deleting the character, or adding one of several possible valid characters for that state and recurse.
Any branch can be abandoned as soon as the distance to valid JSON for that branch exceeds a minimum, if one has been found so far, or according to some heuristics (e.g. no more than four consecutive ['s, and so on), or as soon as it exceeds a maximum allowed distance (say, 5). This last condition is probably essential to avoid snowballing.
A drawback of this approach is that some solutions would require backtracking:
{ "key": "value }
would be accepted up to the end, then a missing " would be added, and finally a missing }, getting a valid JSON:
{ "key": "value }"}
with a distance of 2 (two additions). Perhaps some fast heuristic for backtracking can be found (e.g. when in "wait-for-end-quote", backtrack until the first non-"special" character, and ignore whitespaces).
An important element to assess the viability of a solution would be how is this invalid JSON generated, i.e., is it a truncated JSON? Are some characters damaged? has it some sort of basic structure which is always the same?
User-generated JSON
As per comment, this JSON may be the result of an attempt of hand-building JSON. This on one hand is bad since human beings are very close to a random source when we're talking about errors :-). On the other hand it may be good since the JSON is probably "almost good JSON" with possibly only a few errors: commas left out or added in,
{ "first_name": "John", "last_name": "Doe", "email": "[email protected]", }
Indeed, we might find it useful to at least preprocess the string to remove some common errors:
",\s*} --> "} # Extra comma due to copy-paste
(")\s*[:;.]\s("[^"]"\s:) # Forgot or mistyped a comma between 1 and 2
and since (hopefully) square and curly braces won't appear inside keys or values, we can try and balance them, and/or use them as guidance: e.g. if the curly braces are balanced, assume they're okay and do not add or remove any of them. This has to be weighted against users' error capacity - if they are liable to totally misplace a closing curly brace, then we can't rely on balancing.
(Of course, we can't do anything against a malicious user intentionally feeding malformed JSON designed to either trick the system, or even cause a denial of service through some expression requiring exponential time computation).
At that point we could instead, as you suggested, consider the frequency of JSON control characters, or try recognizing JSON subsequences. This has to be compared against whatever else might come in in that field.
The bad answer ("tell between valid JSON and a string")
In the general case you cannot distinguish with 100% accuracy between strings and JSONs because, while not all JSONs are valid strings, all valid strings are valid JSONs. The encoding of a bare string is that string.
So upon receiving "Hello, world"
, any function would return that this is both a valid string and a valid JSON. Which should it choose?
Otherwise, distinguishing is pretty straightforward. Plug the JSON engine of your choice and try decoding the string as JSON. If you can, that is a JSON string. If you can't, it's a text string:
-- pseudo code
with ( JSON.decode(myString) ) {
if (.isNull()) {
-- Error. Not valid JSON, so it must be a String.
return "String"
}
-- It is a valid JSON, but a String is that, too.
if (.type().equals("String")) {
-- Could return either, too, depending.
return "Ambiguous"
}
}
return "JSON"
This is surely not as efficient as a function that just checks the grammar constraints (you can find the schemas on the home page), but it's a lot more maintainable. You can probably get a FSM by boiling dry the code of a streaming parser and removing all data emission, just returning "true" or "false" (a processing error meaning it's not valid JSON).