有没有一种方法可以使用Jackson(或类似的库)根据现有的Java对象值对JSON对象键进行拼写检查?

时间:2018-12-27 18:44:28

标签: java json jackson

我正在使用Jackson来反序列化JSON对象,到目前为止,该过程已使我成功地将JSON对象转换为Java对象。

但是,我正在想象一个场景,在该场景中,用户在请求的正文中发送JSON,并且一个或多个键的拼写错误。例如,如果Jackson期望{"jurisdiction": "Maine"}但用户拼写错误的密钥并发送{"jrdiction": "Maine"},该怎么办。

有没有一种方法可以使用Jackson来检查Java值的@JsonProperty并将其与请求中的JSON进行比较,然后返回诸如:Property "jrdiction" doesn't exist. Did you mean "jurisdiction"?

我知道,当Java类中不存在某些属性时,杰克逊会抛出UnrecognizedPropertyException。但是,如果我想忽略未知属性(允许用户在JSON对象中发送任何内容),但又进行拼写检查以告知他们某个属性可能拼写错误该怎么办?

3 个答案:

答案 0 :(得分:1)

据我所知,我不认为杰克逊有这种支持,但是可以通过在POJO类中添加以下代码来实现此目的。

@JsonIgnore
private Map<String, Object> additionalProperties = new HashMap<String, Object>();

@JsonAnyGetter
public Map<String, Object> getAdditionalProperties() {
return this.additionalProperties;
}

@JsonAnySetter
public void setAdditionalProperty(String name, Object value) {
this.additionalProperties.put(name, value);
}

此setter和getter将在POJO中不可用的所有不匹配或未知的键/属性添加到地图中。

然后,您可以检查地图的大小,以及地图的大小是否非零,然后您可以查找该未知钥匙最相关的钥匙。如果密钥可以包含多个匹配项,则可能会出现机会。现在,由您决定如何处理它。

答案 1 :(得分:0)

解决方案包括多个部分:

  • 首先,您需要了解所有与json对象中的字段都不匹配的json键。
  • 第二,您需要了解可用的对象json字段,以便找到与拼写错误的json键最接近的匹配项。
  • 最后,您需要一种方法来实际计算不匹配的json键和可用的json字段之间的最接近的匹配。

您可以按照Shivang Agarwal的建议,使用@JsonAnySetter获取与任何JSON对象字段都不匹配的所有JSON数据密钥。

public static class MyParent {
    @JsonProperty("a") protected String jsonA;
    @JsonProperty("b") protected String jsonB;
    // ignored by getJsonPropertyNames()
    protected String internal1;
    @JsonIgnore private Map<String, Object> additionalProperties = new HashMap<String, Object>();
    @JsonAnyGetter public Map<String, Object> getAdditionalProperties() {
        return this.additionalProperties;
    }
    @JsonAnySetter public void setAdditionalProperty(String name, Object value) {
        this.additionalProperties.put(name, value);
    }
}
public static class MyChild extends MyParent {
    @JsonProperty("jurisdiction") protected String jurisdiction;
    // ignored by getJsonPropertyNames()
    protected String internal2;
}

您可以使用以下方法从对象中获取所有可用的JSON字段(以@JsonProperty注释):

private static Collection<String> getJsonPropertyNames(Object o) {
    // might need checking if fields collide
    // Eg:
    //   @JSONProperty String field1;
    //   @JSONProperty("field1") String fieldOne;
    // maybe should be a Set?
    List<String> fields = new ArrayList<>();
    forAllFields(o, (f) -> {
        JsonProperty jprop = f.getAnnotation(JsonProperty.class);
        if (jprop != null) {
            String fieldName = jprop.value();
            if (fieldName == null) {
                fieldName = f.getName();
            }
            fields.add(fieldName);
        }
    });
    return fields;
}

/** For all fields of the given object, including its parent fields */
private static void forAllFields(Object o, Consumer<Field> consumer) {
    Class<?> klass = o.getClass();
    while (klass != null) {
        for (Field f : klass.getDeclaredFields())
            consumer.accept(f);
        klass = klass.getSuperclass();
    }
}

public static void main(String[] args) throws IOException {
    for (String s : getJsonPropertyNames(new MyChild()))
        System.out.println(s);
}

您可以使用以下方法找到最相似的字符串:

我仍然想对我的stringEditDistance方法进行一些测试,但目前可能效果很好。我可能以后再做。

/** finds the nearest matching string from the options
  * using the basic string edit distance where all operations cost 1 */
private static String findNearestMatch(String input, Iterable<String> options) {
    String closestString = null;
    int minDistance = Integer.MAX_VALUE;
    for (String option : options) { 
        int distance = stringEditDistance(input, option, 1, 1, (a, b) -> 1);
        if (distance < minDistance) {
            minDistance = distance;
            closestString = option;
        }
    }
    return closestString;
}

/**
 * NOTE: needs some editing and more testing.
 *
 * Returns the minimum cost to edit the input string into the target string using the given costs for
 * operations.
 * 
 * @param insertCost
 *            the cost to insert a character into the input to bring it closer to the target
 * @param deleteCost
 *            the cost to delete a character from the input to bring it closer to the target
 * @param replaceCostCalculator
 *            a function to calculate the cost to replace a character in the input to bring it close
 *            to the target
 */
public static int stringEditDistance(String input, String target, int insertCost, int deleteCost,
        BiFunction<Character, Character, Integer> replaceCalculator) {
    int[][] dp = new int[input.length() + 1][target.length() + 1];
    for (int i = 0; i <= input.length(); i++)
        dp[i][0] = i;
    for (int j = 0; j <= target.length(); j++)
        dp[0][j] = j;

    for (int i = 0; i < input.length(); i++) {
        char cInput = input.charAt(i);
        for (int j = 0; j < target.length(); j++) {
            char cTarget = target.charAt(j);
            if (cInput == cTarget) {
                dp[i + 1][j + 1] = dp[i][j];
            } else {
                int replace = dp[i][j] + replaceCalculator.apply(cInput, cTarget);
                int insert = dp[i][j + 1] + insertCost;
                int delete = dp[i + 1][j] + deleteCost;
                int min = Math.min(replace, Math.min(insert, delete));
                dp[i + 1][j + 1] = min;
            }
        }
    }
    return dp[input.length()][target.length()];
}

public static void main(String[] args) throws IOException {
    // serialize a json object
    // edit this json to test with other bad input keys
    final String json = "{ \"a\" : \"1\", \"b\" : \"2\", \"jrdiction\" : \"3\" }";
    MyChild child = new ObjectMapper().readerFor(MyChild.class).readValue(json);

    // List<String> jsonProps = getJsonPropertyNames(child);
    // create the list of jsonProps for yourself so you can edit and test easily
    List<String> jsonProps = Arrays.asList("a", "b", "jurisdiction");
    for (Entry<String, Object> e : child.getAdditionalProperties().entrySet()) {
        String nearest = findNearestMatch(e.getKey(), jsonProps);
        System.out.println(e.getKey() + " is closest to " + nearest);
    }
}

答案 2 :(得分:0)

您的问题非常广泛,但我将尝试通过一个简单的例子为您提供一些进一步研究的起点。

让我们假设您有一个像这样的课程:

@Getter @Setter
@AllArgsConstructor
public class MyClass {
    private String name;
    private Integer age;
}

然后您尝试反序列化JSON,如:

{
    "name": "Nomen est Omen",
    "agge": 1
}

我们知道它由于拼写错误的age而失败。为了更好地控制反序列化过程,您可以实现自己的反序列化器,例如:

@SuppressWarnings("serial")
public class MyClassDeserializer extends StdDeserializer<MyClass> {

    public MyClassDeserializer() {
        super((Class<?>) null);
    }

    @Override
    public MyClass deserialize(JsonParser jp, DeserializationContext ctxt) throws IOException, JsonProcessingException {
        JsonNode node = jp.getCodec().readTree(jp);
        // Your logic goes here
        // I suppose here now - for brevity - that there will not be a problem with name
        String name = node.get("name").asText();
        // And that there might be with age which should not be null
        int age;
        String correct = "age";
        try {
            age = node.get("age").asInt();
            return new MyClass(name, age);
        } catch (Exception e) {
            String wrong = magicalStringProximityMethod(correct, node);
            throw new IllegalArgumentException("Property '" + wrong + "' doesn't exist. Did you mean '" + correct + "'?");
        }
    }

    // This returns the closest match in nodes props dor the correct string.
    private String magicalStringProximityMethod(String correct, JsonNode node) {
        Iterator<Entry<String, JsonNode>> iter = node.fields();
        // iterate fields find the closest match
        // Somehow it happems to 'agge' this time
        return "agge";
    }
}

根据实际需要,可能有几种方法可以实现此目的。此实现解决了该问题,因此,仅当无法填充POJO字段时,它才会关心JSON中可能拼写错误的字段。