【问题标题】:Groovy: validate JSON stringGroovy:验证 JSON 字符串
【发布时间】:2018-01-26 20:39:31
【问题描述】:

我需要在 Groovy 中检查一个字符串是否是有效的 JSON。我的第一个想法就是通过new JsonSlurper().parseText(myString) 发送它,如果没有例外,假设它是正确的。

但是,我发现 Groovy 很乐意接受带有 JsonSlurper 的尾随逗号,但接受 JSON doesn't allow trailing commas。是否有一种简单的方法可以在符合官方 JSON 规范的 Groovy 中验证 JSON?

【问题讨论】:

标签: json validation groovy jsonslurper


【解决方案1】:

JsonSlurper 类使用JsonParser 接口实现(JsonParserCharArray 是默认实现)。这些解析器逐个字符地检查当前字符是什么以及它代表的令牌类型。如果您查看第 139 行的 JsonParserCharArray.decodeJsonObject() 方法,您将看到如果解析器看到 } 字符,它会中断循环并完成对 JSON 对象的解码并忽略 } 之后存在的任何内容。

这就是为什么如果您在 JSON 对象前面放置任何无法识别的字符,JsonSlurper 将抛出异常。但是,如果您在} 之后以任何不正确的字符结束您的 JSON 字符串,它将通过,因为解析器甚至不考虑这些字符。

解决方案

您可以考虑使用 JsonOutput.prettyPrint(String json) 方法,如果涉及到它尝试打印的 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}]}

如您所见,获胜者是 Jackson 的 ObjectMapper.readValue() 方法。重要的是 - 它适用于 jackson-databind >= 2.9.0。在这个版本中,他们引入了DeserializationFeature.FAIL_ON_TRAILING_TOKENS,这使得 JSON 解析器按预期工作。如果我们不像上面的脚本那样将此配置特性设置为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 依赖来完成。希望对您有所帮助。

【讨论】:

  • 你为我节省了很多时间。谢谢。
  • @Szymon 感谢调查,我很惊讶 Groovy 的 json 解析器在很多情况下都失败了,直到我弄清楚原因,我相信 Groovy 的动态特性和快速原型设计风格在解析错误时需要一些容差-formed schemas (json,xml...),我发现这在解析不同类型的 HTML 混合 JS 以及处理 REST api 时非常有用。
  • 依靠异常来判断某些东西是否无效总是感觉像是一种反模式。
【解决方案2】:

似乎是 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 )

【讨论】:

    【解决方案3】:

    可以这样验证:

    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()
    

    【讨论】:

    • 不错的一个!如果它是 JsonBuilder 中的方法 isValid() 会是什么?
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2011-02-21
    • 2020-03-30
    • 2011-11-26
    相关资源
    最近更新 更多