web-dev-qa-db-ja.com

Groovy:JSON文字列を検証する

文字列がGroovyで有効なJSONであることを確認する必要があります。私の最初の考えは、new JsonSlurper().parseText(myString)を介してそれを送信することでした。例外がない場合は、それが正しいと仮定しました。

しかし、GroovyはJsonSlurperを使用して末尾のカンマを喜んで受け入れることを発見しましたが、JSON 末尾のカンマは許可していません です。公式のJSON仕様に準拠したGroovyでJSONを検証する簡単な方法はありますか?

9
ewok

JsonSlurper クラスは JsonParser インターフェイス実装を使用します(JsonParserCharArrayがデフォルトです)。これらのパーサーは、文字ごとに現在の文字と、それが表すトークンの種類をチェックします。 139行目で JsonParserCharArray.decodeJsonObject() メソッドを確認すると、パーサーが_}_文字を検出すると、ループが中断され、JSONオブジェクトのデコードが完了してすべて無視されます。 _}_の後に存在します。

そのため、JSONオブジェクトの前に認識できない文字を配置すると、JsonSlurperが例外をスローします。ただし、JSON文字列を_}_の後に誤った文字で終了すると、パーサーはそれらの文字も考慮しないため、文字列は渡されます。

解決

印刷しようとするJSONに関しては、より制限のあるJsonOutput.prettyPrint(String json)メソッドの使用を検討できます(JsonLexerを使用して、ストリーミング方式でJSONトークンを読み取ります)。もし、するなら:

_def jsonString = '{"name": "John", "data": [{"id": 1},{"id": 2}]}...'

JsonOutput.prettyPrint(jsonString)
_

次のような例外がスローされます。

_Exception in thread "main" groovy.json.JsonException: Lexing failed on line: 1, column: 48, while reading '.', no possible valid JSON value or punctuation could be recognized.
    at groovy.json.JsonLexer.nextToken(JsonLexer.Java:83)
    at groovy.json.JsonLexer.hasNext(JsonLexer.Java:233)
    at groovy.json.JsonOutput.prettyPrint(JsonOutput.Java:501)
    at groovy.json.JsonOutput$prettyPrint.call(Unknown Source)
    at org.codehaus.groovy.runtime.callsite.CallSiteArray.defaultCall(CallSiteArray.Java:48)
    at org.codehaus.groovy.runtime.callsite.AbstractCallSite.call(AbstractCallSite.Java:113)
    at org.codehaus.groovy.runtime.callsite.AbstractCallSite.call(AbstractCallSite.Java:125)
    at app.JsonTest.main(JsonTest.groovy:13)
_

しかし、次のような有効なJSONドキュメントを渡した場合:

_def jsonString = '{"name": "John", "data": [{"id": 1},{"id": 2}]}'

JsonOutput.prettyPrint(jsonString)
_

成功します。

良い点は、JSONを検証するために追加の依存関係が必要ないことです。

更新:複数の異なるケースの解決策

さらに調査を行い、3つの異なるソリューションでテストを実行しました。

  • JsonOutput.prettyJson(String json)
  • JsonSlurper.parseText(String json)
  • ObjectMapper.readValue(String json, Class<> type)(_jackson-databind:2.9.3_依存関係を追加する必要があります)

以下のJSONを入力として使用しました。

_def json1 = '{"name": "John", "data": [{"id": 1},{"id": 2},]}'
def json2 = '{"name": "John", "data": [{"id": 1},{"id": 2}],}'
def json3 = '{"name": "John", "data": [{"id": 1},{"id": 2}]},'
def json4 = '{"name": "John", "data": [{"id": 1},{"id": 2}]}... abc'
def json5 = '{"name": "John", "data": [{"id": 1},{"id": 2}]}'
_

期待される結果は、最初の4つのJSONが検証に失敗し、5番目のみが正しいことです。テストするために、次のGroovyスクリプトを作成しました。

_@Grab(group='com.fasterxml.jackson.core', module='jackson-databind', version='2.9.3')

import groovy.json.JsonOutput
import groovy.json.JsonSlurper
import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.databind.DeserializationFeature

def json1 = '{"name": "John", "data": [{"id": 1},{"id": 2},]}'
def json2 = '{"name": "John", "data": [{"id": 1},{"id": 2}],}'
def json3 = '{"name": "John", "data": [{"id": 1},{"id": 2}]},'
def json4 = '{"name": "John", "data": [{"id": 1},{"id": 2}]}... abc'
def json5 = '{"name": "John", "data": [{"id": 1},{"id": 2}]}'

def test1 = { String json ->
    try {
        JsonOutput.prettyPrint(json)
        return "VALID"
    } catch (ignored) {
        return "INVALID"
    }
}

def test2 = { String json ->
    try {
        new JsonSlurper().parseText(json)
        return "VALID"
    } catch (ignored) {
        return "INVALID"
    }
}

ObjectMapper mapper = new ObjectMapper()
mapper.configure(DeserializationFeature.FAIL_ON_TRAILING_TOKENS, true)

def test3 = { String json ->
    try {
        mapper.readValue(json, Map)
        return "VALID"
    } catch (ignored) {
        return "INVALID"
    }
}

def jsons = [json1, json2, json3, json4, json5]
def tests = ['JsonOutput': test1, 'JsonSlurper': test2, 'ObjectMapper': test3]

def result = tests.collectEntries { name, test ->
    [(name): jsons.collect { json ->
        [json: json, status: test(json)]
    }]
}

result.each {
    println "${it.key}:"
    it.value.each {
        println " ${it.status}: ${it.json}"
    }
    println ""
}
_

そしてここに結果があります:

_JsonOutput:
 VALID: {"name": "John", "data": [{"id": 1},{"id": 2},]}
 VALID: {"name": "John", "data": [{"id": 1},{"id": 2}],}
 VALID: {"name": "John", "data": [{"id": 1},{"id": 2}]},
 INVALID: {"name": "John", "data": [{"id": 1},{"id": 2}]}... abc
 VALID: {"name": "John", "data": [{"id": 1},{"id": 2}]}

JsonSlurper:
 INVALID: {"name": "John", "data": [{"id": 1},{"id": 2},]}
 VALID: {"name": "John", "data": [{"id": 1},{"id": 2}],}
 VALID: {"name": "John", "data": [{"id": 1},{"id": 2}]},
 VALID: {"name": "John", "data": [{"id": 1},{"id": 2}]}... abc
 VALID: {"name": "John", "data": [{"id": 1},{"id": 2}]}

ObjectMapper:
 INVALID: {"name": "John", "data": [{"id": 1},{"id": 2},]}
 INVALID: {"name": "John", "data": [{"id": 1},{"id": 2}],}
 INVALID: {"name": "John", "data": [{"id": 1},{"id": 2}]},
 INVALID: {"name": "John", "data": [{"id": 1},{"id": 2}]}... abc
 VALID: {"name": "John", "data": [{"id": 1},{"id": 2}]}
_

ご覧のとおり、勝者はジャクソンのObjectMapper.readValue()メソッドです。重要なこと-_jackson-databind_> = _2.9.0_で動作します。このバージョンでは、JSONパーサーを期待どおりに機能させる_DeserializationFeature.FAIL_ON_TRAILING_TOKENS_を導入しました。上記のスクリプトのように、この構成機能をtrueに設定しない場合、ObjectMapperは誤った結果を生成します。

_ObjectMapper:
 INVALID: {"name": "John", "data": [{"id": 1},{"id": 2},]}
 INVALID: {"name": "John", "data": [{"id": 1},{"id": 2}],}
 VALID: {"name": "John", "data": [{"id": 1},{"id": 2}]},
 VALID: {"name": "John", "data": [{"id": 1},{"id": 2}]}... abc
 VALID: {"name": "John", "data": [{"id": 1},{"id": 2}]}
_

このテストでGroovyの標準ライブラリが失敗することに驚きました。幸いなことに、これは_jackson-databind:2.9.x_依存関係で実行できます。それが役に立てば幸い。

4
Szymon Stepniak

groovy jsonパーサーのバグまたは機能のようです

別のjsonパーサーを試してください

私はsnakeyamlを使用しているので、jsonとyamlがサポートされていますが、インターネット上で他のJavaベースのjsonパーサーライブラリを見つけることができます

@Grab(group='org.yaml', module='snakeyaml', version='1.19')

def jsonString = '''{"a":1,"b":2}...'''

//no error in the next line
def json1 = new groovy.json.JsonSlurper().parseText( jsonString )
//the following line fails
def json2 = new org.yaml.snakeyaml.Yaml().load( jsonString )
2
daggett

このように検証できます:

assert JsonOutput.toJson(new JsonSlurper().parseText(myString)).replaceAll("\\s", "") ==
            myString.replaceAll("\\s", "")

または少しクリーナー:

String.metaClass.isJson << { ->
    def normalize = { it.replaceAll("\\s", "") }

    try {
        normalize(delegate) == normalize(JsonOutput.toJson(new JsonSlurper().parseText(delegate)))
    } catch (e) {
        false
    }
}

assert '{"key":"value"}'.isJson()
assert !''.isJson()
0
Nitsan Avni