Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

JsonValue Ignored #208

Open
mbenz89 opened this issue Nov 17, 2021 · 3 comments
Open

JsonValue Ignored #208

mbenz89 opened this issue Nov 17, 2021 · 3 comments
Labels
enhancement New feature or request help wanted Extra attention is needed

Comments

@mbenz89
Copy link

mbenz89 commented Nov 17, 2021

Hi!

I have a case like this:

   "SomeObject" : {
      "type" : "object",
      "properties" : {
        "foo" : {
          "$ref" : "#/$defs/AnotherObject(String)"
      },
        "bar" : {
          "$ref" : "#/$defs/AnotherObject"
      }
 },
    "AnotherObject(String)" : {
      "type" : "object",
      "properties" : {
        "value" : "boolean"
      }
 },
  AnotherObject : {
    "type" : "object",
      "properties" : {
        "value" : {}
      }
}

My code looks like this:

class SomeObject{
 public AnotherObject<Boolean> foo;
 public AnotherObject<Object> bar;
}
class AnotherObject<T>{
 @JsonValue
 public T getValue();
}

I would expect that the JsonValue property leads to inlining the value of AnotherObject. Also for the case where the type parameter T is java.lang.Object the value type is not printed: `"value": {}".

Any help on this? I would implement the type conversion myself but I was only able to change properties in the type AnotherType but not entirely replace properties of that type with the generic type of the used AnotherType field.

It would be awesome if you could provide me with a stub or hint on how to implement a custom Module to replace "$ref" : "#/$defs/AnotherObject(String)" with "type": "string" (for example).

@mbenz89
Copy link
Author

mbenz89 commented Nov 17, 2021

These are the options I use:

            <options>
                        <!-- https://victools.github.io/jsonschema-generator/#generator-options -->
                        <preset>PLAIN_JSON</preset>
                        <enabled>
                            <option>NULLABLE_ARRAY_ITEMS_ALLOWED</option>
                            <option>DEFINITIONS_FOR_ALL_OBJECTS</option>
                            <option>MAP_VALUES_AS_ADDITIONAL_PROPERTIES</option>
                            <option>FIELDS_DERIVED_FROM_ARGUMENTFREE_METHODS</option>
                            <option>NONSTATIC_NONVOID_NONGETTER_METHODS</option>
                        </enabled>
                    </options>
                    <modules>
                        <module>
                            <name>Jackson</name>
                            <options>
                                <option>FLATTENED_ENUMS_FROM_JSONVALUE</option>
                                <option>FLATTENED_ENUMS_FROM_JSONPROPERTY</option>
                            </options>
                        </module>
                    .... some custom plugins

@CarstenWickner
Copy link
Member

Hi @mbenz89,

I've looked into this a bit and came up with a CustomDefinitionProvider, which I considered introducing under a new standard Option, but got some doubts on whether I'm fully understanding the use-cases for the @JsonValue annotation.
If you only have atomic values (e.g. a String/int/double), the following should go in the right direction:

public class CustomJsonValueDefinitionProvider implements CustomDefinitionProviderV2 {

    @Override
    public CustomDefinition provideCustomSchemaDefinition(ResolvedType javaType, SchemaGenerationContext context) {
        ResolvedTypeWithMembers typeWithMembers = context.getTypeContext().resolveWithMembers(javaType);
        List<ResolvedMember<?>> jsonValueMembers = Stream
                .concat(Stream.of(typeWithMembers.getMemberFields()), Stream.of(typeWithMembers.getMemberMethods()))
                .filter(member -> Optional.ofNullable(member.get(JsonValue.class)).filter(JsonValue::value).isPresent())
                .collect(Collectors.toList());

        if (jsonValueMembers.size() != 1) {
            // no unambiguous @JsonValue target could be detected
            return null;
        }
        ResolvedType underlyingValueType = jsonValueMembers.get(0).getType();
        ObjectNode customNode = context.createStandardDefinitionReference(underlyingValueType, javaType.equals(underlyingValueType) ? this : null);
        return new CustomDefinition(customNode, CustomDefinition.DefinitionType.INLINE, CustomDefinition.AttributeInclusion.YES);
    }
}

That would be applied (in code) like this:

builder.forTypesInGeneral()
    .withCustomDefinitionProvider(new CustomJsonValueDefinitionProvider())

Can a @JsonValue annotated method also produce a JSON object (or array) structure?
I'd only feel comfortable with this, If there were a considerable number of unit tests to verify the correct behaviour. Unfortunately, I'm currently lacking the time to look into this more closely. A PR is always welcome though. 😉


As to your question regarding AnotherObject<Object> producing {} instead of { "type": "object" }:
That is on purpose. An Object could easily represent a { "type": "string" } or { "type": "number" }. Consequently, the "any schema" {} seems to be the appropriate choice. 🤷

@mbenz89
Copy link
Author

mbenz89 commented Nov 25, 2021

Hi @CarstenWickner.

Thanks for the help! Your implementation works just fine for atomic values!

Can a @JsonValue annotated method also produce a JSON object (or array) structure?

Unfortunately, @JsonValue also works for complex objects. Currently, these seem to be inlined as { }. I would need them to be inlined as #/$def/<TYPE>. Can you give me a hint on how to change your implementation to do that correctly and still work for atomic values?

CustomDefinition.AttributeInclusion.YES

I had to change this to NO. Otherwise, the description annotated to AnotherObject<X> will be attached to the inline value. This doesn't make sense (at least in my case) as the description targets the Object AnthorObject and not the inline value annotated with JsonValue.

I'd only feel comfortable with this, If there were a considerable number of unit tests to verify the correct behavior. Unfortunately, I'm currently lacking the time to look into this more closely. A PR is always welcome though. wink

Happy to propose a PR with some test cases as soon as I got it working properly :).

As to your question regarding AnotherObject producing {} instead of { "type": "object" }:
That is on purpose. An Object could easily represent a { "type": "string" } or { "type": "number" }. Consequently, the "any schema" {} seems to be the appropriate choice.

Makes total sense, thanks for elaborating!


A further question. I also have type definitions in my schema that look like this Map(String,AnotherObject<String>) and Pair(AnotherObject<String>,AnotherObject<String>). I would like to convert both to json objects. I was just wondering why it isn't done for Map already and if in this special case of JsonValue types for Map, something breaks.

@CarstenWickner CarstenWickner added enhancement New feature or request help wanted Extra attention is needed labels Dec 3, 2021
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request help wanted Extra attention is needed
Development

No branches or pull requests

2 participants