jimblackler / jsonschemafriend

A JSON Schema loader and validator, delivered as a Java library.
Apache License 2.0
49 stars 23 forks source link

Support for custom types when document to be validated passed via a Map #71

Closed mikaelstaldal closed 6 months ago

mikaelstaldal commented 6 months ago

It would be useful to be able to support custom Java types when passing document to be validated via a map.

Currently, you get a UnexpectedTypeError if the input map structure contains anything else than Map, List, Number, String, Boolean or null.

I propose that you can register custom Java types and map them to a $ref in the schema.

Ideally, the validator should check for custom types first, so that you can have custom handling of e.g. java.math.BigInteger or java.math.BigDecimal (which are Numbers), and retain the standard handling of all other Numbers.

big-andy-coates commented 6 months ago

Hi @mikaelstaldal and thanks for the suggestion. @jimblackler may have other options here, but it seems to be that your suggestion would be conflating two separate tasks:

  1. Transforming an instance of a custom Java type into a map of its properties.
  2. Validating the map of properties conforms to a JSON schema.

This library focuses on the second part: validating data against a JSON schema.

What you're proposing, if I'm reading your suggestion correctly, is to enhance the library to also be able to perform the first part: extracting the JSON properties from a Java object.

There are many ways of transforming from Java object tp JSON properties and many libraries dedicated to the task, (org.json, gson, Jackson, usejson, etc). These often require, or support, their own custom patterns and annotations on the Java object being transformed.

If this library were to handle both steps it would either need to choose 1 of the implementations or somehow handle all of them, massively increasing the complexity and dependencies of this project.

By focusing only the second step in this project, users are free to use any other library they choose for the first step. Classic separation of concern :).

For example, let's say you've a source map containing an entry with a "address" string key and a value that's a custom Address type. Then, with the correct naming conventions or annotations on the Address type, you can transform the source map to one compatible with this library by using, for example, Jackson with the following snippet:

Map<String, Object> outputMap = new ObjectMapper().readValue(sourceMap, Map.class);

The resultant outputMap could then be passed to this library for validation.

In the interest of keeping open issues tidy I'm therefore going to close this issue for now. @mikaelstaldal, if I've misunderstood your suggestion, or you'd like to discuss more, then please reopen, maybe with a worked example to help us understand your suggestion. @jimblackler may also choose to chim in.

jimblackler commented 6 months ago

Thanks Andy. There's nothing stopping clients from pre-converting data into the expected format, which is designed to be the most Java-y way of descripbing data that could be transformed into JSON.

mikaelstaldal commented 5 months ago
1. Transforming an instance of a custom Java type into a map of its properties.

2. Validating the map of properties conforms to a JSON schema.

This library focuses on the second part: validating data against a JSON schema.

What you're proposing, if I'm reading your suggestion correctly, is to enhance the library to also be able to perform the first part: extracting the JSON properties from a Java object.

No, that's not what I want. I was not planning to use this feature to register a bunch of custom domain model classes. I agree that it makes more sense to use Jackson or something similar for that purpose.

I was planning to use it to register a couple of extra "primitive" types which are not possible to explicitly express in JSON. E.g. java.math.BigInteger as a distinct type, or byte[] for blobs.

big-andy-coates commented 5 months ago

Hi @mikaelstaldal, can you explain (maybe with an example) what you would expect this library to do with the BigInteger or byte[]?

mikaelstaldal commented 5 months ago

I would expect it to map them to specified $ref:s in the schema.

{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "type": "object",
  "properties": {
    "a_string": {
      "type": "string"
    },
    "an_integer": {
      "type": "integer"
    },
    "a_big_integer": {
      "$ref": "#/$defs/big_integer"
    },
    "a_byte_array": {
      "$ref": "#/$defs/byte_array"
    }
  },
  "$defs": {
    "big_integer": {
      "description": "Big integer"
    },
    "byte_array": {
      "description": "Byte array"
    }
  }
}
      Map<String, Object> myMap = Map.of(
         "a_string", "Hello, world!",
         "an_integer", 17,
         "a_big_integer", new java.math.BigInteger("123456678"),
         "a_byte_array", "ABCD".getBytes()
       );

      Schema schema = schemaStore.loadSchema(...);
      schema.registerType(java.lang.BigInteger.class, "#/$defs/big_integer");
      schema.registerType(byte[].class, "#/$defs/byte_array");
      Validator validator = new Validator();
      validator.validate(schema, myMap);
big-andy-coates commented 5 months ago

@mikaelstaldal apologies, but I'm still struggling to understand what you're asking for.

Your example doesn't define any properties in the bigint or bytes type definitions. Can you correct this please?

I'm unsure why you need a special type for storing a BigInteger in JSON. Why not just store as a json 'integer'?

How are you encoding the byte[] in your JSON? This could, for example, be a Base64 encoded string.

Importantly, how do you propose this library knows how to convert from byte[] to something like a Base64 encoded string so that it can validate the value?

mikaelstaldal commented 5 months ago

@mikaelstaldal apologies, but I'm still struggling to understand what you're asking for.

Your example doesn't define any properties in the bigint or bytes type definitions.

That's on purpose, since they are primitives.

I'm unsure why you need a special type for storing a BigInteger in JSON. Why not just store as a json 'integer'?

I want to distinguish them from regular integers.

Importantly, how do you propose this library knows how to convert from byte[] to something like a Base64 encoded string so that it can validate the value?

It doesn't need to validate the value, that validation happens before, when the Map is constructed.