-
Notifications
You must be signed in to change notification settings - Fork 91
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
Map-backed OAS model classes from annotations #2021
Conversation
47b4491
to
ba11b2a
Compare
@MikeEdgar Let me know when you ready. I thin we need to get this in and roll forward it there are any issues |
Signed-off-by: Michael Edgar <[email protected]>
Signed-off-by: Michael Edgar <[email protected]>
Signed-off-by: Michael Edgar <[email protected]>
Signed-off-by: Michael Edgar <[email protected]>
Signed-off-by: Michael Edgar <[email protected]>
Signed-off-by: Michael Edgar <[email protected]>
Quality Gate passedIssues Measures |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's go !
if (Constructible.class.isAssignableFrom(desiredType)) { | ||
return readObject((Class<? extends Constructible>) desiredType, (O) node); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we need to check the node type before casting to (O)
here. Not sure what we do want if it turns out node
is a value or array, just fall through to fromJson(node)
?
I think this is the cause of this exception (though I haven't yet worked out what in my test prompted it):
java.lang.ClassCastException: com.fasterxml.jackson.databind.node.NullNode incompatible with com.fasterxml.jackson.databind.node.ObjectNode
at io.smallrye.openapi.runtime.io.JacksonJsonIO.properties(JacksonJsonIO.java:33)
at io.smallrye.openapi.runtime.io.ModelIO.readObject(ModelIO.java:262)
at io.smallrye.openapi.runtime.io.ModelIO.readValue(ModelIO.java:376) <-- I assume `node` is a `NullNode` at this point
at io.smallrye.openapi.runtime.io.ModelIO.readJson(ModelIO.java:340)
at io.smallrye.openapi.runtime.io.ModelIO.readJson(ModelIO.java:309)
at io.smallrye.openapi.runtime.io.ModelIO.readObject(ModelIO.java:272)
at io.smallrye.openapi.runtime.io.ModelIO.readValue(ModelIO.java:376)
at io.smallrye.openapi.runtime.io.ModelIO.readJson(ModelIO.java:316)
at io.smallrye.openapi.runtime.io.ModelIO.readObject(ModelIO.java:272)
at io.smallrye.openapi.runtime.io.OpenAPIDefinitionIO.readObject(OpenAPIDefinitionIO.java:58)
at io.smallrye.openapi.runtime.io.OpenAPIDefinitionIO.readObject(OpenAPIDefinitionIO.java:12)
at java.base/java.util.Optional.map(Optional.java:260)
at io.smallrye.openapi.runtime.io.ModelIO.readValue(ModelIO.java:390)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I was able to reproduce it also with this input.
---
openapi: 3.1.0
info:
title: My Title
version: "1.0.0"
contact: null
paths: {}
Maybe something like this to avoid it?
if (Constructible.class.isAssignableFrom(desiredType)) {
if (jsonIO().isObject(node)) {
return readObject((Class<? extends Constructible>) desiredType, (O) node);
} else if (jsonIO().isNull(node)) { // new `isNull` method
return null;
} else {
// Log that value type cannot be read as a desiredType.getName();
return null;
}
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That would work.
My initial thought was
if (Constructible.class.isAssignableFrom(desiredType) && jsonIO().isObject(node)) {
return readObject((Class<? extends Constructible>) desiredType, (O) node);
}
and allow unexpected types to fall through to jsonIO().fromJson(node)
, but that causes unexpected types to end up in the model.
Dropping the unexpected type on read would match the old IO objects, though SchemaIO
allowed unexpected types in and did a type check in its getter methods. It looks like with this PR that SchemaIO
no longer does that which I'd have thought would have caused tests to fail so I might be missing something there.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would also expect a failure if invalid types were allowed in before. Ultimately they should be written back out as long as the version is 3.1.0. Let me know what you find on that. I suppose it should be OK unless they've set an alternate schema dialect, in which case everything should pass through as-is.
|
||
writeCodeLn(writer, 2, "return getProperties(", valueTypeParam, ");"); | ||
} else { | ||
writeCodeLn(writer, 2, "return ", property.getPropertyMethod, "(\"", property.name, "\");"); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this generates a call to BaseExtensibleModel.getProperty(String)
which does not do any type checking (vs getProperty(String, Class<?>)
which returns null
if the property is not the right type.
I guess that's ok as long as we're certain that what's in the map is valid (e.g. we validate it on read). What's the overall strategy for ensuring we don't get ClassCastException
s at runtime if the user feeds us an invalid openapi.yaml?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Generally, we should be protected from invalid input by the expected types declared via the DataType
instances. The generated SmallRyeOASModels
class contains a map of all Constructible
s as keys, and values containing the known properties and their DataType
s. It ends up being similar to the logic that was in SchemaIO
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For Schema
we did two things:
- In
SchemaIO
when reading, try to read the JSON as the desired type- fall back to reading it as a basic type if it can't be read as a desired type
- In
SchemaImpl
when getting a value, check if it's the correct type before returning it- if it's not the correct type, return
null
- The actual value could still be obtained with
Schema.get(String)
- if it's not the correct type, return
I think this part is doing the getting, and unlike SchemaImpl
, it isn't checking the type before returning the value, resulting in a ClassCastException
when Java checks the return type of the generated getter method against the value being returned if it's not of the expected type.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we can either implement the same logic in the getters as SchemaImpl
did, or ensure that any invalid data is dropped when reading as I think the old IO classes would do.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think +1 to dropping invalid data in IO as well as the type checking on the getters. I think we are close to that in the current IO code, with the exception of the class cast bug you noted above. I'll put up a PR with the changes.
valueTypeParam = property.valueType + CLASS; | ||
} | ||
|
||
writeCodeLn(writer, 2, "return getProperties(", valueTypeParam, ");"); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This change means that some methods which could previously return null
will instead return an empty map.
APIResponses.getAPIResponses()
Content.getMediaTypes()
Callback.getPathItems()
Paths.getPathItems()
SecurityRequirement.getSchemes()
I'm unsure if this is worth worrying about, but it is probably worth noting as a change in behaviour since 3.x. It did require changes in a couple of places in OpenLiberty.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Right, these objects are quasi-maps themselves so non-null seems to make sense there if the map returned is a map form of the object itself.
A similar change that I don't think is important to fix but we should note: some model classes used to extend |
public Info readObject(O node) { | ||
IoLogging.logger.singleJsonNode("Info"); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
With the removal of this readObject
, the call will now go to ModelIO.readObject
which will throw an UnsupportedOperationException
.
It looks like only OpenAPIDefinitionIO
still has readObject
implemented which delegates to ModelIO.readObject(Class, O)
. I assume that one was kept to keep backwards compatibility?
OpenLiberty was previously using InfoIO.readObject
to parse an Info
object for our configuration. I can change that to InfoIO.readObject(Info.class, node);
but I am wondering if that's a reasonable way to parse an arbitrary model object.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think that is a reasonable way to parse it. The only thing remaining with special/overridden JSON handling is the SchemaIO
.
We'll need to talk through an approach for how a parsing API should look more generally so there can be a stable contract that is exposed for it. I think it's close, with ModelIO.readObject(Class, O)
basically having the needed functionality, but there is still some further refactoring to do I think.
This was an artifact of MP OpenAPI 1.x where several of the interfaces extended |
No, it was not a problem to remove. |
This PR replaces the current *Impl model classes with a set of map-backed models generated from annotations (private to the project). This brings a few benefits -
The bulk of the change is the changing of the Impl model classes to extend the new models, deprecating the old models, and replacing existing instances of
new XyzImpl()
withOASFactory.createXyz()
. A newmodel
module is added along with atools/model-apt
module to contain the base model and APT processor respectively.This currently depends on a SNAPSHOT build of the TCK and can be merged once version 4.0.2 of the spec is released.