diff --git a/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/util/JsonToJsonPathsConverter.groovy b/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/util/JsonToJsonPathsConverter.groovy index b602f3bc23..e87c5c59f5 100644 --- a/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/util/JsonToJsonPathsConverter.groovy +++ b/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/util/JsonToJsonPathsConverter.groovy @@ -84,8 +84,11 @@ class JsonToJsonPathsConverter { if (bodyMatchers?.hasMatchers()) { bodyMatchers.jsonPathMatchers().each { BodyMatcher matcher -> try { - context.delete(matcher.path()) - removeTrailingContainers(matcher, context) + def entry = entry(context, matcher.path()) + if (entry != null) { + context.delete(matcher.path()) + removeTrailingContainers(matcher, context) + } } catch (RuntimeException e) { if (log.isDebugEnabled()) { log.debug("Exception occurred while trying to delete path [${matcher.path()}]", e) @@ -96,6 +99,17 @@ class JsonToJsonPathsConverter { return jsonCopy } + private static def entry(DocumentContext context, String path) { + try { + return context.read(path) + } catch (Exception ex) { + if (log.isDebugEnabled()) { + log.debug("Exception occurred while trying to retrieve element via path [${path}]", ex) + } + return null + } + } + /** * Related to #391. The converted body looks different when done via the String notation than * it does when done via a map notation. When working with String body and when matchers @@ -106,22 +120,26 @@ class JsonToJsonPathsConverter { * defining body... */ private static void removeTrailingContainers(BodyMatcher matcher, DocumentContext context) { - if (matcher.path().contains(ANY_ARRAY_NOTATION_IN_JSONPATH)) { - String pathWithoutAnyArray = matcher.path().substring(0, matcher.path().lastIndexOf(ANY_ARRAY_NOTATION_IN_JSONPATH)) - def object = context.read(pathWithoutAnyArray) - if (object instanceof Iterable && containsOnlyEmptyElements(object)) { - String pathToDelete = pathToDelete(pathWithoutAnyArray) - context.delete(pathToDelete) - } else { - String lastParent = matcher.path().substring(0, matcher.path().lastIndexOf(".")) - def lastParentObject = context.read(lastParent) - if (lastParentObject instanceof Iterable && containsOnlyEmptyElements(lastParentObject)) { - context.delete(lastParent) - } + String pathWithoutAnyArray = matcher.path().contains(ANY_ARRAY_NOTATION_IN_JSONPATH) ? matcher.path().substring(0, matcher.path().lastIndexOf(ANY_ARRAY_NOTATION_IN_JSONPATH)) : matcher.path() + def object = entry(context, pathWithoutAnyArray) + // object got removed and it was the only element + // let's get its parent and see if it contains an empty element + if (isIterable(object) && containsOnlyEmptyElements(object)) { + String pathToDelete = pathToDelete(pathWithoutAnyArray) + context.delete(pathToDelete) + } else { + String lastParent = matcher.path().substring(0, matcher.path().lastIndexOf(".")) + def lastParentObject = context.read(lastParent) + if (isIterable(lastParentObject) && containsOnlyEmptyElements(lastParentObject)) { + context.delete(lastParent) } } } + private static boolean isIterable(Object object) { + return object instanceof Iterable || object instanceof Map + } + private static String pathToDelete(String pathWithoutAnyArray) { // we can't remove root return pathWithoutAnyArray == '$' ? '$[*]' : pathWithoutAnyArray diff --git a/spring-cloud-contract-verifier/src/test/groovy/org/springframework/cloud/contract/verifier/builder/MethodBodyBuilderSpec.groovy b/spring-cloud-contract-verifier/src/test/groovy/org/springframework/cloud/contract/verifier/builder/MethodBodyBuilderSpec.groovy index 81a292e711..248b3ecb01 100644 --- a/spring-cloud-contract-verifier/src/test/groovy/org/springframework/cloud/contract/verifier/builder/MethodBodyBuilderSpec.groovy +++ b/spring-cloud-contract-verifier/src/test/groovy/org/springframework/cloud/contract/verifier/builder/MethodBodyBuilderSpec.groovy @@ -981,4 +981,49 @@ DocumentContext parsedJson = JsonPath.parse(json); "JaxRsClientJUnitMethodBodyBuilder" | { Contract dsl -> new JaxRsClientJUnitMethodBodyBuilder(dsl, properties) } } + @Issue("#727") + def "should not leave empty arrays [#methodBuilderName]"() { + given: + Contract contractDsl = Contract.make { + request { + method 'GET' + url '/list' + } + response { + status 200 + body( + [ + content: [ + one: "two", + two: "two", + three: [ + six: "seven" + ] + ] + ] + ) + bodyMatchers { + jsonPath('$.content.three.six', byRegex(".*seven.*")) + jsonPath('$.content.one', byRegex(".*two.*")) + } + } + } + MethodBodyBuilder builder = methodBuilder(contractDsl) + BlockBuilder blockBuilder = new BlockBuilder(" ") + when: + builder.appendTo(blockBuilder) + then: + String test = blockBuilder.toString() + SyntaxChecker.tryToCompileWithoutCompileStatic(methodBuilderName, test) + !test.contains('''.isEmpty()''') + and: + stubMappingIsValidWireMockStub(contractDsl) + where: + methodBuilderName | methodBuilder + "MockMvcSpockMethodBuilder" | { Contract dsl -> new MockMvcSpockMethodRequestProcessingBodyBuilder(dsl, properties) } + "MockMvcJUnitMethodBuilder" | { Contract dsl -> new MockMvcJUnitMethodBodyBuilder(dsl, properties) } + "JaxRsClientSpockMethodRequestProcessingBodyBuilder" | { Contract dsl -> new JaxRsClientSpockMethodRequestProcessingBodyBuilder(dsl, properties) } + "JaxRsClientJUnitMethodBodyBuilder" | { Contract dsl -> new JaxRsClientJUnitMethodBodyBuilder(dsl, properties) } + } + }