diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 000000000..e35e5615a --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,123 @@ +# Repository guidance for coding agents + +## Scope and priorities + +- This file applies to the entire repository. If a more specific `AGENTS.md` is added below a module, follow that file for work in that subtree. +- Solve the requested problem completely, add or update tests, and validate the smallest relevant scope before handing off. +- Preserve unrelated work in the checkout. Inspect `git status` before and after changes, and do not rewrite user-owned changes. +- Use existing project patterns and production implementations; do not replace behavior with mocks, placeholders, or test-only shortcuts. + +## Repository overview + +This is a Gradle multi-project Java repository for generating Java bindings from Conjure API definitions. + +- `conjure-java-core`: the main Conjure object, service, Dialogue, Jersey, and Undertow source generators. Most generator logic and golden/snapshot tests live here. +- `conjure-java`: the command-line application, with `ConjureJavaCli` as its entry point. +- `conjure-lib`: shared runtime types used by generated code and other modules. +- `conjure-undertow-lib`, `conjure-java-undertow-runtime`: Undertow APIs and runtime behavior. +- `conjure-undertow-annotations`, `conjure-undertow-processor`, and `conjure-undertow-processor-example`: annotation API, annotation processor, and its integration example. +- `conjure-java-client-verifier` and `conjure-java-server-verifier`: compatibility verification against Conjure verification test cases. +- `versions.props` and `versions.lock`: centralized dependency declarations and their resolved lock state. + +Consult `readme.md` for supported generator features and wire-format expectations. Treat `build.gradle` and `settings.gradle` as authoritative when older documentation disagrees with the current build. + +## Build and toolchains + +- Always invoke the checked-in wrapper from the repository root: `./gradlew ...`. +- The build configures and may provision multiple JDKs. At present it compiles with Java 25, targets Java 17 libraries, runs tests on Java 25, and uses a Java 21 Gradle daemon. A first Gradle invocation may therefore download toolchains. +- Java compilation uses `-Werror`, Error Prone, Checkstyle, dependency checks, and Palantir Java Format. Fix findings rather than weakening or globally suppressing checks. +- Format Java and Gradle sources with `./gradlew format`. Do not manually reformat generated fixtures that are intentionally excluded from formatting. +- Keep dependencies in `versions.props`; do not add inline versions to module build files. Regenerate `versions.lock` with the appropriate Gradle consistent-versions task when dependency declarations change, and review the resulting lock diff. +- `.circleci/config.yml` is generated from `.circleci/template.sh`; do not edit the generated configuration directly. + +## Generated code and snapshots + +Generator output is part of the test contract and is checked into the repository. + +- `conjure-java-core/src/integrationInput/java` contains generated integration fixtures. +- Java-like files under `src/test/resources` include golden outputs for generators and the Undertow annotation processor. +- A normal test run compares generated output with these files. If a generator change intentionally alters output, run `./gradlew test -Drecreate=true`, then carefully review every changed and newly created fixture. +- Never use `-Drecreate=true` merely to make a failing snapshot test pass. Confirm that each output change follows from the intended production change, then rerun the affected tests without recreation. +- Generator changes should normally cover representative combinations of options, not only a single happy-path fixture. Update the parameterized cases or processor samples where appropriate. + +## Implementation conventions + +- Follow the existing Java style and copyright headers in neighboring files. +- Preserve Conjure wire compatibility and generated API semantics: generated values are immutable, fields are non-null, optionals represent absence, enums and unions tolerate unknown values, and collection behavior depends on generator options. +- Keep generator option behavior consistent across objects and relevant service backends (Dialogue, Jersey, and Undertow). When adding an option, wire it through `Options`, CLI/configuration handling, generation, documentation, and snapshots as applicable. +- Prefer focused, package-local changes. Avoid exposing a new public API when an existing internal abstraction is sufficient. +- Published library modules use API compatibility checks. Do not update compatibility baselines simply to silence a failure; first determine whether the API change is intended. +- Tests use JUnit 5 and AssertJ. Annotation-processor tests also use compile-testing and checked-in generated sources. Match the style of adjacent tests. + +## Validation + +Run validation in increasing scope, choosing tasks that cover every modified module: + +```bash +./gradlew :conjure-java-core:test --tests 'fully.qualified.TestClass' +./gradlew :conjure-java-core:test +./gradlew :conjure-undertow-processor:test +./gradlew format +./gradlew build +``` + +- Use the analogous `::test` task for other modules; do not run unrelated verifier suites for a narrowly scoped change unless shared behavior is affected. +- Run the full `./gradlew build` for cross-module, dependency, generator-output, or public-API changes. CI runs the build with parallel workers and fails if it leaves tracked files modified. +- After formatting, fixture recreation, or a full build, inspect `git status --short` and the complete diff. Generated output or lock-file changes must be included only when intentional. +- If environment or toolchain setup prevents a task from running, report the exact command and failure; do not claim the task passed. + +## Change hygiene + +- Keep changes narrowly related to the request and include regression coverage for bug fixes. +- Update `readme.md` when user-visible flags or documented generator behavior changes. +- User-facing changes may require a changelog entry under `changelog/@unreleased`; follow the existing changelog YAML schema and repository release conventions. +- In the final handoff, summarize behavior changed, tests run, and any validation that could not be completed. + +## Code Conventions + +### Code Style +- All files must end with a newline +- Use `./gradlew spotlessApply` to auto-format code (Palantir Java format) +- Compiler treats warnings as errors (`-Werror`) +- Prefer imported classes over fully qualified names (`List` not `java.util.List`) +- All dependencies must be explicitly declared (no implicit transitive dependencies) +- Java library target: 17, distribution target: 21 +- Avoid unnecessary comments - most code is self-documenting, only explain non-obvious details or high-level concepts (applies to javadoc and inline comments). When you do include comments, make sure they are concise. +- **String concatenation whitespace** - When a string literal is split across multiple lines (typically by a formatter or for readability), ensure the boundary between adjacent literals preserves required whitespace. Place a leading space at the start of the second segment (e.g., `"first part" + " second part"`). Applies to error messages, log messages, exception arguments, test strings, and any other user- or developer-visible concatenated text. + +### Safe Logging +- Use the `com.palantir.logsafe` safe-logging library to ensure log safety. Exception messages and arguments must be explicitly marked as safe or unsafe so that logging systems (e.g., Grafana) never leak sensitive data. Classes and parameters can also carry `@Safe` or `@Unsafe` annotations to declare their safety at the type level. +- **Exceptions**: Never throw raw JDK exceptions (`IllegalStateException`, `IllegalArgumentException`, `NullPointerException`, `RuntimeException`, etc.). Use their safe-logging equivalents from `com.palantir.logsafe.exceptions` instead: `SafeIllegalStateException`, `SafeIllegalArgumentException`, `SafeNullPointerException`, `SafeRuntimeException`, `SafeUncheckedIoException`, etc. + - **Typed Conjure errors are exempt**: Conjure-defined error types already declare the safety of each argument via `safe-args` and `unsafe-args` fields, so they don't need `Safe*` equivalents. +- **Exception arguments**: Pass structured arguments using `SafeArg.of("name", value)` (for safe-to-log values) or `UnsafeArg.of("name", value)` (for sensitive values) instead of interpolating values into the message string. When choosing `SafeArg` vs `UnsafeArg`: + - **Safe**: RIDs with random/UUID locators (e.g. object type RIDs, dataset RIDs), durations, counts, class names + - **Unsafe**: User-inputted string identifiers (e.g. object type IDs, branch names/IDs, property type IDs, search queries), exception messages, file paths, PII + - **Do not log**: Credentials, tokens, private keys, authorization headers + - When in doubt, use `UnsafeArg` and flag it for the author to review +- **Preconditions**: Use `com.palantir.logsafe.Preconditions` instead of Guava's `com.google.common.base.Preconditions` for the same safety guarantees. +- **Logging**: Use `SafeLogger` / `SafeLoggerFactory` instead of SLF4J's `LoggerFactory` directly. + +### Streams and Optionals +- Use `Stream.toList()` instead of `Stream.collect(Collectors.toList())` where possible. +- **Small streams (default)**: Readability wins. + - When using optionals in a stream, prefer `.mapMulti(Optional::ifPresent)` over `.flatMap(Optional::stream)`. + - No `.map(Optional::get).filter(Objects::nonNull)`. It has 2 steps which is more error-prone. +- **Large streams or store-level code**: Allocation wins. For nullable values, prefer either: + - `.map(...).filter(Objects::nonNull)` directly on the nullable value, or + - A plain `for`/`forEach` loop instead of `stream().map(...)` when the pipeline is simple. + +### Union Types +- For sealed unions (Conjure union types or sealed interfaces): + - Use `switch` expressions when handling multiple variants differently (ensures exhaustiveness) + - Use `instanceof` for guard clauses where only one variant needs handling and others early-return + +### Collections +- Prefer Guava's immutable collections (`ImmutableSet`, `ImmutableList`, `ImmutableMap`) with builder pattern over mutable collection classes (`LinkedHashSet`, `ArrayList`, `HashMap +`). + - JDK immutable utilities like `Set.of()` or `List.of()` are fine. +- **Method boundaries vs. internal use**: An immutable or unmodifiable view of a collection is strongly preferred at method boundaries (e.g., return types and parameters). For inter +mediate results within a method scope (e.g., a collection immediately passed into a builder), using `Collectors.toSet()` or similar mutable collector is acceptable for efficiency. + +### Testing Conventions +- Prefer Mockito's `eq()` matchers over `any()` for higher test coverage, unless the assertion is not relevant for the test. +- Name the `@BeforeEach` setup method `before`. diff --git a/conjure-java-client-verifier/build.gradle b/conjure-java-client-verifier/build.gradle index 3bddaa15d..1bab56089 100644 --- a/conjure-java-client-verifier/build.gradle +++ b/conjure-java-client-verifier/build.gradle @@ -67,6 +67,7 @@ project('verification-server-api') { api 'com.palantir.dialogue:dialogue-serde' implementation 'com.fasterxml.jackson.core:jackson-annotations' + implementation 'com.fasterxml.jackson.core:jackson-core' implementation 'com.fasterxml.jackson.core:jackson-databind' implementation 'com.google.code.findbugs:jsr305' implementation 'com.google.errorprone:error_prone_annotations' @@ -84,4 +85,3 @@ tasks.withType(JavaCompile).matching { it.name == "compileTestJava" }.configureE check('Slf4jLogsafeArgs', net.ltgt.gradle.errorprone.CheckSeverity.OFF) } } - diff --git a/conjure-java-core/build.gradle b/conjure-java-core/build.gradle index 91ac604f7..a968a4df1 100644 --- a/conjure-java-core/build.gradle +++ b/conjure-java-core/build.gradle @@ -53,6 +53,7 @@ dependencies { implementation project(':conjure-lib') implementation 'com.fasterxml.jackson.core:jackson-annotations' + implementation 'com.fasterxml.jackson.core:jackson-core' implementation 'com.google.code.findbugs:jsr305' implementation 'com.google.errorprone:error_prone_annotations' implementation 'com.palantir.common:streams' @@ -97,6 +98,7 @@ dependencies { integrationInputImplementation project(':conjure-lib') integrationInputImplementation project(':conjure-undertow-lib') integrationInputImplementation 'com.fasterxml.jackson.core:jackson-annotations' + integrationInputImplementation 'com.fasterxml.jackson.core:jackson-core' integrationInputImplementation 'com.fasterxml.jackson.core:jackson-databind' integrationInputImplementation 'com.google.code.findbugs:jsr305' integrationInputImplementation 'com.google.errorprone:error_prone_annotations' diff --git a/conjure-java-core/src/integrationInput/java/allexamples/com/palantir/product/AliasAsMapKeyExample.java b/conjure-java-core/src/integrationInput/java/allexamples/com/palantir/product/AliasAsMapKeyExample.java index 104e651f3..4ffaa6ae9 100644 --- a/conjure-java-core/src/integrationInput/java/allexamples/com/palantir/product/AliasAsMapKeyExample.java +++ b/conjure-java-core/src/integrationInput/java/allexamples/com/palantir/product/AliasAsMapKeyExample.java @@ -178,18 +178,25 @@ public static Builder builder() { public static final class Builder { boolean _buildInvoked; + @JsonSetter(value = "strings", nulls = Nulls.SKIP, contentNulls = Nulls.FAIL) private Map strings = new LinkedHashMap<>(); + @JsonSetter(value = "rids", nulls = Nulls.SKIP, contentNulls = Nulls.FAIL) private Map rids = new LinkedHashMap<>(); + @JsonSetter(value = "bearertokens", nulls = Nulls.SKIP, contentNulls = Nulls.FAIL) private Map bearertokens = new LinkedHashMap<>(); + @JsonSetter(value = "integers", nulls = Nulls.SKIP, contentNulls = Nulls.FAIL) private Map integers = new LinkedHashMap<>(); + @JsonSetter(value = "safelongs", nulls = Nulls.SKIP, contentNulls = Nulls.FAIL) private Map safelongs = new LinkedHashMap<>(); + @JsonSetter(value = "datetimes", nulls = Nulls.SKIP, contentNulls = Nulls.FAIL) private Map datetimes = new LinkedHashMap<>(); + @JsonSetter(value = "uuids", nulls = Nulls.SKIP, contentNulls = Nulls.FAIL) private Map uuids = new LinkedHashMap<>(); private Builder() {} @@ -206,7 +213,6 @@ public Builder from(AliasAsMapKeyExample other) { return this; } - @JsonSetter(value = "strings", nulls = Nulls.SKIP, contentNulls = Nulls.FAIL) public Builder strings(@Nonnull Map strings) { checkNotBuilt(); this.strings = new LinkedHashMap<>(Preconditions.checkNotNull(strings, "strings cannot be null")); @@ -225,7 +231,6 @@ public Builder strings(StringAliasExample key, ManyFieldExample value) { return this; } - @JsonSetter(value = "rids", nulls = Nulls.SKIP, contentNulls = Nulls.FAIL) public Builder rids(@Nonnull Map rids) { checkNotBuilt(); this.rids = new LinkedHashMap<>(Preconditions.checkNotNull(rids, "rids cannot be null")); @@ -244,7 +249,6 @@ public Builder rids(RidAliasExample key, ManyFieldExample value) { return this; } - @JsonSetter(value = "bearertokens", nulls = Nulls.SKIP, contentNulls = Nulls.FAIL) public Builder bearertokens(@Nonnull Map bearertokens) { checkNotBuilt(); this.bearertokens = @@ -264,7 +268,6 @@ public Builder bearertokens(BearerTokenAliasExample key, ManyFieldExample value) return this; } - @JsonSetter(value = "integers", nulls = Nulls.SKIP, contentNulls = Nulls.FAIL) public Builder integers(@Nonnull Map integers) { checkNotBuilt(); this.integers = new LinkedHashMap<>(Preconditions.checkNotNull(integers, "integers cannot be null")); @@ -283,7 +286,6 @@ public Builder integers(IntegerAliasExample key, ManyFieldExample value) { return this; } - @JsonSetter(value = "safelongs", nulls = Nulls.SKIP, contentNulls = Nulls.FAIL) public Builder safelongs(@Nonnull Map safelongs) { checkNotBuilt(); this.safelongs = new LinkedHashMap<>(Preconditions.checkNotNull(safelongs, "safelongs cannot be null")); @@ -302,7 +304,6 @@ public Builder safelongs(SafeLongAliasExample key, ManyFieldExample value) { return this; } - @JsonSetter(value = "datetimes", nulls = Nulls.SKIP, contentNulls = Nulls.FAIL) public Builder datetimes(@Nonnull Map datetimes) { checkNotBuilt(); this.datetimes = new LinkedHashMap<>(Preconditions.checkNotNull(datetimes, "datetimes cannot be null")); @@ -321,7 +322,6 @@ public Builder datetimes(DateTimeAliasExample key, ManyFieldExample value) { return this; } - @JsonSetter(value = "uuids", nulls = Nulls.SKIP, contentNulls = Nulls.FAIL) public Builder uuids(@Nonnull Map uuids) { checkNotBuilt(); this.uuids = new LinkedHashMap<>(Preconditions.checkNotNull(uuids, "uuids cannot be null")); diff --git a/conjure-java-core/src/integrationInput/java/allexamples/com/palantir/product/AnyMapExample.java b/conjure-java-core/src/integrationInput/java/allexamples/com/palantir/product/AnyMapExample.java index 7c621d7d5..41d11b4a6 100644 --- a/conjure-java-core/src/integrationInput/java/allexamples/com/palantir/product/AnyMapExample.java +++ b/conjure-java-core/src/integrationInput/java/allexamples/com/palantir/product/AnyMapExample.java @@ -95,6 +95,7 @@ public static Builder builder() { public static final class Builder { boolean _buildInvoked; + @JsonSetter(value = "items", nulls = Nulls.SKIP, contentNulls = Nulls.FAIL) private Map items = new LinkedHashMap<>(); private Builder() {} @@ -105,7 +106,6 @@ public Builder from(AnyMapExample other) { return this; } - @JsonSetter(value = "items", nulls = Nulls.SKIP, contentNulls = Nulls.FAIL) public Builder items(@Nonnull Map items) { checkNotBuilt(); this.items = new LinkedHashMap<>(Preconditions.checkNotNull(items, "items cannot be null")); diff --git a/conjure-java-core/src/integrationInput/java/allexamples/com/palantir/product/EmptyUnionTypeExample.java b/conjure-java-core/src/integrationInput/java/allexamples/com/palantir/product/EmptyUnionTypeExample.java index a1ec49933..4b04044ac 100644 --- a/conjure-java-core/src/integrationInput/java/allexamples/com/palantir/product/EmptyUnionTypeExample.java +++ b/conjure-java-core/src/integrationInput/java/allexamples/com/palantir/product/EmptyUnionTypeExample.java @@ -3,14 +3,23 @@ import com.fasterxml.jackson.annotation.JsonAnyGetter; import com.fasterxml.jackson.annotation.JsonAnySetter; import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.annotation.JsonTypeInfo; import com.fasterxml.jackson.annotation.JsonValue; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.JsonToken; +import com.fasterxml.jackson.core.util.JsonParserSequence; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonDeserializer; +import com.fasterxml.jackson.databind.JsonMappingException; +import com.fasterxml.jackson.databind.MapperFeature; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.deser.ResolvableDeserializer; +import com.fasterxml.jackson.databind.util.TokenBuffer; import com.palantir.logsafe.Preconditions; import com.palantir.logsafe.Safe; import com.palantir.logsafe.SafeArg; import com.palantir.logsafe.exceptions.SafeIllegalArgumentException; +import java.io.IOException; import java.util.Collections; import java.util.HashMap; import java.util.Map; @@ -21,6 +30,7 @@ import javax.annotation.processing.Generated; @Generated("com.palantir.conjure.java.types.UnionGenerator") +@JsonDeserialize(using = EmptyUnionTypeExample.Deserializer.class) public final class EmptyUnionTypeExample { private final Base value; @@ -123,13 +133,6 @@ public interface Completed_StageVisitorBuilder { Visitor build(); } - @JsonTypeInfo( - use = JsonTypeInfo.Id.NAME, - include = JsonTypeInfo.As.EXISTING_PROPERTY, - property = "type", - visible = true, - defaultImpl = UnknownWrapper.class) - @JsonIgnoreProperties(ignoreUnknown = true) private interface Base { T accept(Visitor visitor); } @@ -193,4 +196,127 @@ public String toString() { return "UnknownWrapper{type: " + type + ", value: " + value + '}'; } } + + static final class Deserializer extends JsonDeserializer implements ResolvableDeserializer { + private static final Class[] VARIANT_TYPES = new Class[] {}; + + private volatile JsonDeserializer[] deserializers; + + @Override + public void resolve(DeserializationContext context) throws JsonMappingException { + deserializers = new JsonDeserializer[VARIANT_TYPES.length]; + } + + @Override + public boolean isCachable() { + return true; + } + + @Override + public EmptyUnionTypeExample deserialize(JsonParser parser, DeserializationContext context) throws IOException { + if (!parser.isExpectedStartObjectToken()) { + return context.reportInputMismatch( + EmptyUnionTypeExample.class, "Expected a JSON object for union deserialization"); + } + JsonToken firstToken = parser.nextToken(); + if (firstToken == JsonToken.FIELD_NAME && isTypeField(parser.currentName(), context)) { + if (parser.nextToken() != JsonToken.VALUE_STRING) { + return context.reportInputMismatch( + EmptyUnionTypeExample.class, "Union discriminator 'type' must be a string"); + } + String type = parser.getText(); + parser.nextToken(); + return deserializeSelected(parser, context, type); + } + return deserializeBuffered(parser, context); + } + + private EmptyUnionTypeExample deserializeBuffered(JsonParser parser, DeserializationContext context) + throws IOException { + try (TokenBuffer buffer = context.bufferForInputBuffering(parser)) { + buffer.writeStartObject(); + JsonToken token = parser.currentToken(); + while (token == JsonToken.FIELD_NAME) { + String fieldName = parser.currentName(); + JsonToken valueToken = parser.nextToken(); + if (isTypeField(fieldName, context)) { + if (valueToken != JsonToken.VALUE_STRING) { + return context.reportInputMismatch( + EmptyUnionTypeExample.class, "Union discriminator 'type' must be a string"); + } + String type = parser.getText(); + parser.nextToken(); + try (JsonParser bufferedParser = buffer.asParser(parser)) { + JsonParser combinedParser = + JsonParserSequence.createFlattened(true, bufferedParser, parser); + combinedParser.nextToken(); + return deserializeSelected(combinedParser, context, type); + } + } + buffer.writeFieldName(fieldName); + buffer.copyCurrentStructure(parser); + token = parser.nextToken(); + } + if (token != JsonToken.END_OBJECT) { + return context.reportInputMismatch( + EmptyUnionTypeExample.class, + "Expected the end of a JSON object while deserializing a union"); + } + } + return context.reportInputMismatch(EmptyUnionTypeExample.class, "Union discriminator 'type' is required"); + } + + private static boolean isTypeField(String fieldName, DeserializationContext context) { + return "type".equals(fieldName) + || (context.isEnabled(MapperFeature.ACCEPT_CASE_INSENSITIVE_PROPERTIES) + && "type".equalsIgnoreCase(fieldName)); + } + + private EmptyUnionTypeExample deserializeSelected( + JsonParser parser, DeserializationContext context, String type) throws IOException { + int variantIndex = + switch (type) { + default -> -1; + }; + if (variantIndex < 0) { + return deserializeUnknown(parser, context, type); + } + JsonDeserializer deserializer = deserializers[variantIndex]; + if (deserializer == null) { + deserializer = resolveDeserializer(context, variantIndex); + } + return new EmptyUnionTypeExample((Base) deserializer.deserialize(parser, context)); + } + + private synchronized JsonDeserializer resolveDeserializer(DeserializationContext context, int variantIndex) + throws JsonMappingException { + JsonDeserializer deserializer = deserializers[variantIndex]; + if (deserializer == null) { + deserializer = context.findRootValueDeserializer(context.constructType(VARIANT_TYPES[variantIndex])); + JsonDeserializer[] updated = deserializers.clone(); + updated[variantIndex] = deserializer; + deserializers = updated; + } + return deserializer; + } + + private static EmptyUnionTypeExample deserializeUnknown( + JsonParser parser, DeserializationContext context, String type) throws IOException { + Map values = new HashMap<>(); + if (parser.currentToken() == JsonToken.START_OBJECT) { + parser.nextToken(); + } + while (parser.currentToken() == JsonToken.FIELD_NAME) { + String fieldName = parser.currentName(); + parser.nextToken(); + values.put(fieldName, context.readValue(parser, Object.class)); + parser.nextToken(); + } + if (parser.currentToken() != JsonToken.END_OBJECT) { + return context.reportInputMismatch( + EmptyUnionTypeExample.class, "Expected the end of a JSON object while deserializing a union"); + } + return new EmptyUnionTypeExample(new UnknownWrapper(type, values)); + } + } } diff --git a/conjure-java-core/src/integrationInput/java/allexamples/com/palantir/product/ExternalLongUnionExample.java b/conjure-java-core/src/integrationInput/java/allexamples/com/palantir/product/ExternalLongUnionExample.java index 8d99c73b4..c0759009e 100644 --- a/conjure-java-core/src/integrationInput/java/allexamples/com/palantir/product/ExternalLongUnionExample.java +++ b/conjure-java-core/src/integrationInput/java/allexamples/com/palantir/product/ExternalLongUnionExample.java @@ -6,14 +6,23 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.annotation.JsonSetter; -import com.fasterxml.jackson.annotation.JsonSubTypes; -import com.fasterxml.jackson.annotation.JsonTypeInfo; import com.fasterxml.jackson.annotation.JsonTypeName; import com.fasterxml.jackson.annotation.JsonValue; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.JsonToken; +import com.fasterxml.jackson.core.util.JsonParserSequence; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonDeserializer; +import com.fasterxml.jackson.databind.JsonMappingException; +import com.fasterxml.jackson.databind.MapperFeature; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.deser.ResolvableDeserializer; +import com.fasterxml.jackson.databind.util.TokenBuffer; import com.palantir.logsafe.Preconditions; import com.palantir.logsafe.Safe; import com.palantir.logsafe.SafeArg; import com.palantir.logsafe.exceptions.SafeIllegalArgumentException; +import java.io.IOException; import java.util.Collections; import java.util.HashMap; import java.util.Map; @@ -25,6 +34,7 @@ /** A union of a safe long. */ @Generated("com.palantir.conjure.java.types.UnionGenerator") +@JsonDeserialize(using = ExternalLongUnionExample.Deserializer.class) public final class ExternalLongUnionExample { private final Base value; @@ -157,19 +167,12 @@ public interface Completed_StageVisitorBuilder { Visitor build(); } - @JsonTypeInfo( - use = JsonTypeInfo.Id.NAME, - include = JsonTypeInfo.As.EXISTING_PROPERTY, - property = "type", - visible = true, - defaultImpl = UnknownWrapper.class) - @JsonSubTypes(@JsonSubTypes.Type(SafeLongWrapper.class)) - @JsonIgnoreProperties(ignoreUnknown = true) private interface Base { T accept(Visitor visitor); } @JsonTypeName("safeLong") + @JsonIgnoreProperties(ignoreUnknown = true) private static final class SafeLongWrapper implements Base { private final long value; @@ -273,4 +276,132 @@ public String toString() { return "UnknownWrapper{type: " + type + ", value: " + value + '}'; } } + + static final class Deserializer extends JsonDeserializer + implements ResolvableDeserializer { + private static final Class[] VARIANT_TYPES = new Class[] {SafeLongWrapper.class}; + + private volatile JsonDeserializer[] deserializers; + + @Override + public void resolve(DeserializationContext context) throws JsonMappingException { + deserializers = new JsonDeserializer[VARIANT_TYPES.length]; + } + + @Override + public boolean isCachable() { + return true; + } + + @Override + public ExternalLongUnionExample deserialize(JsonParser parser, DeserializationContext context) + throws IOException { + if (!parser.isExpectedStartObjectToken()) { + return context.reportInputMismatch( + ExternalLongUnionExample.class, "Expected a JSON object for union deserialization"); + } + JsonToken firstToken = parser.nextToken(); + if (firstToken == JsonToken.FIELD_NAME && isTypeField(parser.currentName(), context)) { + if (parser.nextToken() != JsonToken.VALUE_STRING) { + return context.reportInputMismatch( + ExternalLongUnionExample.class, "Union discriminator 'type' must be a string"); + } + String type = parser.getText(); + parser.nextToken(); + return deserializeSelected(parser, context, type); + } + return deserializeBuffered(parser, context); + } + + private ExternalLongUnionExample deserializeBuffered(JsonParser parser, DeserializationContext context) + throws IOException { + try (TokenBuffer buffer = context.bufferForInputBuffering(parser)) { + buffer.writeStartObject(); + JsonToken token = parser.currentToken(); + while (token == JsonToken.FIELD_NAME) { + String fieldName = parser.currentName(); + JsonToken valueToken = parser.nextToken(); + if (isTypeField(fieldName, context)) { + if (valueToken != JsonToken.VALUE_STRING) { + return context.reportInputMismatch( + ExternalLongUnionExample.class, "Union discriminator 'type' must be a string"); + } + String type = parser.getText(); + parser.nextToken(); + try (JsonParser bufferedParser = buffer.asParser(parser)) { + JsonParser combinedParser = + JsonParserSequence.createFlattened(true, bufferedParser, parser); + combinedParser.nextToken(); + return deserializeSelected(combinedParser, context, type); + } + } + buffer.writeFieldName(fieldName); + buffer.copyCurrentStructure(parser); + token = parser.nextToken(); + } + if (token != JsonToken.END_OBJECT) { + return context.reportInputMismatch( + ExternalLongUnionExample.class, + "Expected the end of a JSON object while deserializing a union"); + } + } + return context.reportInputMismatch( + ExternalLongUnionExample.class, "Union discriminator 'type' is required"); + } + + private static boolean isTypeField(String fieldName, DeserializationContext context) { + return "type".equals(fieldName) + || (context.isEnabled(MapperFeature.ACCEPT_CASE_INSENSITIVE_PROPERTIES) + && "type".equalsIgnoreCase(fieldName)); + } + + private ExternalLongUnionExample deserializeSelected( + JsonParser parser, DeserializationContext context, String type) throws IOException { + int variantIndex = + switch (type) { + case "safeLong" -> 0; + default -> -1; + }; + if (variantIndex < 0) { + return deserializeUnknown(parser, context, type); + } + JsonDeserializer deserializer = deserializers[variantIndex]; + if (deserializer == null) { + deserializer = resolveDeserializer(context, variantIndex); + } + return new ExternalLongUnionExample((Base) deserializer.deserialize(parser, context)); + } + + private synchronized JsonDeserializer resolveDeserializer(DeserializationContext context, int variantIndex) + throws JsonMappingException { + JsonDeserializer deserializer = deserializers[variantIndex]; + if (deserializer == null) { + deserializer = context.findRootValueDeserializer(context.constructType(VARIANT_TYPES[variantIndex])); + JsonDeserializer[] updated = deserializers.clone(); + updated[variantIndex] = deserializer; + deserializers = updated; + } + return deserializer; + } + + private static ExternalLongUnionExample deserializeUnknown( + JsonParser parser, DeserializationContext context, String type) throws IOException { + Map values = new HashMap<>(); + if (parser.currentToken() == JsonToken.START_OBJECT) { + parser.nextToken(); + } + while (parser.currentToken() == JsonToken.FIELD_NAME) { + String fieldName = parser.currentName(); + parser.nextToken(); + values.put(fieldName, context.readValue(parser, Object.class)); + parser.nextToken(); + } + if (parser.currentToken() != JsonToken.END_OBJECT) { + return context.reportInputMismatch( + ExternalLongUnionExample.class, + "Expected the end of a JSON object while deserializing a union"); + } + return new ExternalLongUnionExample(new UnknownWrapper(type, values)); + } + } } diff --git a/conjure-java-core/src/integrationInput/java/allexamples/com/palantir/product/LargeUnionExample.java b/conjure-java-core/src/integrationInput/java/allexamples/com/palantir/product/LargeUnionExample.java index 2f3db496f..926bdd446 100644 --- a/conjure-java-core/src/integrationInput/java/allexamples/com/palantir/product/LargeUnionExample.java +++ b/conjure-java-core/src/integrationInput/java/allexamples/com/palantir/product/LargeUnionExample.java @@ -6,13 +6,22 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.annotation.JsonSetter; -import com.fasterxml.jackson.annotation.JsonSubTypes; -import com.fasterxml.jackson.annotation.JsonTypeInfo; import com.fasterxml.jackson.annotation.JsonTypeName; import com.fasterxml.jackson.annotation.JsonValue; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.JsonToken; +import com.fasterxml.jackson.core.util.JsonParserSequence; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonDeserializer; +import com.fasterxml.jackson.databind.JsonMappingException; +import com.fasterxml.jackson.databind.MapperFeature; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.deser.ResolvableDeserializer; +import com.fasterxml.jackson.databind.util.TokenBuffer; import com.palantir.logsafe.Preconditions; import com.palantir.logsafe.Safe; import com.palantir.logsafe.exceptions.SafeIllegalArgumentException; +import java.io.IOException; import java.util.Collections; import java.util.HashMap; import java.util.Map; @@ -21,6 +30,7 @@ import javax.annotation.processing.Generated; @Generated("com.palantir.conjure.java.types.UnionGenerator") +@JsonDeserialize(using = LargeUnionExample.Deserializer.class) public final class LargeUnionExample { private final Base value; @@ -977,121 +987,12 @@ public interface Visitor { T visitUnknown(@Safe String unknownType, Object unknownValue); } - @JsonTypeInfo( - use = JsonTypeInfo.Id.NAME, - include = JsonTypeInfo.As.EXISTING_PROPERTY, - property = "type", - visible = true, - defaultImpl = UnknownWrapper.class) - @JsonSubTypes({ - @JsonSubTypes.Type(U0Wrapper.class), - @JsonSubTypes.Type(U1Wrapper.class), - @JsonSubTypes.Type(U2Wrapper.class), - @JsonSubTypes.Type(U3Wrapper.class), - @JsonSubTypes.Type(U4Wrapper.class), - @JsonSubTypes.Type(U5Wrapper.class), - @JsonSubTypes.Type(U6Wrapper.class), - @JsonSubTypes.Type(U7Wrapper.class), - @JsonSubTypes.Type(U8Wrapper.class), - @JsonSubTypes.Type(U9Wrapper.class), - @JsonSubTypes.Type(U10Wrapper.class), - @JsonSubTypes.Type(U11Wrapper.class), - @JsonSubTypes.Type(U12Wrapper.class), - @JsonSubTypes.Type(U13Wrapper.class), - @JsonSubTypes.Type(U14Wrapper.class), - @JsonSubTypes.Type(U15Wrapper.class), - @JsonSubTypes.Type(U16Wrapper.class), - @JsonSubTypes.Type(U17Wrapper.class), - @JsonSubTypes.Type(U18Wrapper.class), - @JsonSubTypes.Type(U19Wrapper.class), - @JsonSubTypes.Type(U20Wrapper.class), - @JsonSubTypes.Type(U21Wrapper.class), - @JsonSubTypes.Type(U22Wrapper.class), - @JsonSubTypes.Type(U23Wrapper.class), - @JsonSubTypes.Type(U24Wrapper.class), - @JsonSubTypes.Type(U25Wrapper.class), - @JsonSubTypes.Type(U26Wrapper.class), - @JsonSubTypes.Type(U27Wrapper.class), - @JsonSubTypes.Type(U28Wrapper.class), - @JsonSubTypes.Type(U29Wrapper.class), - @JsonSubTypes.Type(U30Wrapper.class), - @JsonSubTypes.Type(U31Wrapper.class), - @JsonSubTypes.Type(U32Wrapper.class), - @JsonSubTypes.Type(U33Wrapper.class), - @JsonSubTypes.Type(U34Wrapper.class), - @JsonSubTypes.Type(U35Wrapper.class), - @JsonSubTypes.Type(U36Wrapper.class), - @JsonSubTypes.Type(U37Wrapper.class), - @JsonSubTypes.Type(U38Wrapper.class), - @JsonSubTypes.Type(U39Wrapper.class), - @JsonSubTypes.Type(U40Wrapper.class), - @JsonSubTypes.Type(U41Wrapper.class), - @JsonSubTypes.Type(U42Wrapper.class), - @JsonSubTypes.Type(U43Wrapper.class), - @JsonSubTypes.Type(U44Wrapper.class), - @JsonSubTypes.Type(U45Wrapper.class), - @JsonSubTypes.Type(U46Wrapper.class), - @JsonSubTypes.Type(U47Wrapper.class), - @JsonSubTypes.Type(U48Wrapper.class), - @JsonSubTypes.Type(U49Wrapper.class), - @JsonSubTypes.Type(U50Wrapper.class), - @JsonSubTypes.Type(U51Wrapper.class), - @JsonSubTypes.Type(U52Wrapper.class), - @JsonSubTypes.Type(U53Wrapper.class), - @JsonSubTypes.Type(U54Wrapper.class), - @JsonSubTypes.Type(U55Wrapper.class), - @JsonSubTypes.Type(U56Wrapper.class), - @JsonSubTypes.Type(U57Wrapper.class), - @JsonSubTypes.Type(U58Wrapper.class), - @JsonSubTypes.Type(U59Wrapper.class), - @JsonSubTypes.Type(U60Wrapper.class), - @JsonSubTypes.Type(U61Wrapper.class), - @JsonSubTypes.Type(U62Wrapper.class), - @JsonSubTypes.Type(U63Wrapper.class), - @JsonSubTypes.Type(U64Wrapper.class), - @JsonSubTypes.Type(U65Wrapper.class), - @JsonSubTypes.Type(U66Wrapper.class), - @JsonSubTypes.Type(U67Wrapper.class), - @JsonSubTypes.Type(U68Wrapper.class), - @JsonSubTypes.Type(U69Wrapper.class), - @JsonSubTypes.Type(U70Wrapper.class), - @JsonSubTypes.Type(U71Wrapper.class), - @JsonSubTypes.Type(U72Wrapper.class), - @JsonSubTypes.Type(U73Wrapper.class), - @JsonSubTypes.Type(U74Wrapper.class), - @JsonSubTypes.Type(U75Wrapper.class), - @JsonSubTypes.Type(U76Wrapper.class), - @JsonSubTypes.Type(U77Wrapper.class), - @JsonSubTypes.Type(U78Wrapper.class), - @JsonSubTypes.Type(U79Wrapper.class), - @JsonSubTypes.Type(U80Wrapper.class), - @JsonSubTypes.Type(U81Wrapper.class), - @JsonSubTypes.Type(U82Wrapper.class), - @JsonSubTypes.Type(U83Wrapper.class), - @JsonSubTypes.Type(U84Wrapper.class), - @JsonSubTypes.Type(U85Wrapper.class), - @JsonSubTypes.Type(U86Wrapper.class), - @JsonSubTypes.Type(U87Wrapper.class), - @JsonSubTypes.Type(U88Wrapper.class), - @JsonSubTypes.Type(U89Wrapper.class), - @JsonSubTypes.Type(U90Wrapper.class), - @JsonSubTypes.Type(U91Wrapper.class), - @JsonSubTypes.Type(U92Wrapper.class), - @JsonSubTypes.Type(U93Wrapper.class), - @JsonSubTypes.Type(U94Wrapper.class), - @JsonSubTypes.Type(U95Wrapper.class), - @JsonSubTypes.Type(U96Wrapper.class), - @JsonSubTypes.Type(U97Wrapper.class), - @JsonSubTypes.Type(U98Wrapper.class), - @JsonSubTypes.Type(U99Wrapper.class), - @JsonSubTypes.Type(U100Wrapper.class) - }) - @JsonIgnoreProperties(ignoreUnknown = true) private interface Base { T accept(Visitor visitor); } @JsonTypeName("u0") + @JsonIgnoreProperties(ignoreUnknown = true) private static final class U0Wrapper implements Base { private final String value; @@ -1137,6 +1038,7 @@ public String toString() { } @JsonTypeName("u1") + @JsonIgnoreProperties(ignoreUnknown = true) private static final class U1Wrapper implements Base { private final String value; @@ -1182,6 +1084,7 @@ public String toString() { } @JsonTypeName("u2") + @JsonIgnoreProperties(ignoreUnknown = true) private static final class U2Wrapper implements Base { private final String value; @@ -1227,6 +1130,7 @@ public String toString() { } @JsonTypeName("u3") + @JsonIgnoreProperties(ignoreUnknown = true) private static final class U3Wrapper implements Base { private final String value; @@ -1272,6 +1176,7 @@ public String toString() { } @JsonTypeName("u4") + @JsonIgnoreProperties(ignoreUnknown = true) private static final class U4Wrapper implements Base { private final String value; @@ -1317,6 +1222,7 @@ public String toString() { } @JsonTypeName("u5") + @JsonIgnoreProperties(ignoreUnknown = true) private static final class U5Wrapper implements Base { private final String value; @@ -1362,6 +1268,7 @@ public String toString() { } @JsonTypeName("u6") + @JsonIgnoreProperties(ignoreUnknown = true) private static final class U6Wrapper implements Base { private final String value; @@ -1407,6 +1314,7 @@ public String toString() { } @JsonTypeName("u7") + @JsonIgnoreProperties(ignoreUnknown = true) private static final class U7Wrapper implements Base { private final String value; @@ -1452,6 +1360,7 @@ public String toString() { } @JsonTypeName("u8") + @JsonIgnoreProperties(ignoreUnknown = true) private static final class U8Wrapper implements Base { private final String value; @@ -1497,6 +1406,7 @@ public String toString() { } @JsonTypeName("u9") + @JsonIgnoreProperties(ignoreUnknown = true) private static final class U9Wrapper implements Base { private final String value; @@ -1542,6 +1452,7 @@ public String toString() { } @JsonTypeName("u10") + @JsonIgnoreProperties(ignoreUnknown = true) private static final class U10Wrapper implements Base { private final String value; @@ -1587,6 +1498,7 @@ public String toString() { } @JsonTypeName("u11") + @JsonIgnoreProperties(ignoreUnknown = true) private static final class U11Wrapper implements Base { private final String value; @@ -1632,6 +1544,7 @@ public String toString() { } @JsonTypeName("u12") + @JsonIgnoreProperties(ignoreUnknown = true) private static final class U12Wrapper implements Base { private final String value; @@ -1677,6 +1590,7 @@ public String toString() { } @JsonTypeName("u13") + @JsonIgnoreProperties(ignoreUnknown = true) private static final class U13Wrapper implements Base { private final String value; @@ -1722,6 +1636,7 @@ public String toString() { } @JsonTypeName("u14") + @JsonIgnoreProperties(ignoreUnknown = true) private static final class U14Wrapper implements Base { private final String value; @@ -1767,6 +1682,7 @@ public String toString() { } @JsonTypeName("u15") + @JsonIgnoreProperties(ignoreUnknown = true) private static final class U15Wrapper implements Base { private final String value; @@ -1812,6 +1728,7 @@ public String toString() { } @JsonTypeName("u16") + @JsonIgnoreProperties(ignoreUnknown = true) private static final class U16Wrapper implements Base { private final String value; @@ -1857,6 +1774,7 @@ public String toString() { } @JsonTypeName("u17") + @JsonIgnoreProperties(ignoreUnknown = true) private static final class U17Wrapper implements Base { private final String value; @@ -1902,6 +1820,7 @@ public String toString() { } @JsonTypeName("u18") + @JsonIgnoreProperties(ignoreUnknown = true) private static final class U18Wrapper implements Base { private final String value; @@ -1947,6 +1866,7 @@ public String toString() { } @JsonTypeName("u19") + @JsonIgnoreProperties(ignoreUnknown = true) private static final class U19Wrapper implements Base { private final String value; @@ -1992,6 +1912,7 @@ public String toString() { } @JsonTypeName("u20") + @JsonIgnoreProperties(ignoreUnknown = true) private static final class U20Wrapper implements Base { private final String value; @@ -2037,6 +1958,7 @@ public String toString() { } @JsonTypeName("u21") + @JsonIgnoreProperties(ignoreUnknown = true) private static final class U21Wrapper implements Base { private final String value; @@ -2082,6 +2004,7 @@ public String toString() { } @JsonTypeName("u22") + @JsonIgnoreProperties(ignoreUnknown = true) private static final class U22Wrapper implements Base { private final String value; @@ -2127,6 +2050,7 @@ public String toString() { } @JsonTypeName("u23") + @JsonIgnoreProperties(ignoreUnknown = true) private static final class U23Wrapper implements Base { private final String value; @@ -2172,6 +2096,7 @@ public String toString() { } @JsonTypeName("u24") + @JsonIgnoreProperties(ignoreUnknown = true) private static final class U24Wrapper implements Base { private final String value; @@ -2217,6 +2142,7 @@ public String toString() { } @JsonTypeName("u25") + @JsonIgnoreProperties(ignoreUnknown = true) private static final class U25Wrapper implements Base { private final String value; @@ -2262,6 +2188,7 @@ public String toString() { } @JsonTypeName("u26") + @JsonIgnoreProperties(ignoreUnknown = true) private static final class U26Wrapper implements Base { private final String value; @@ -2307,6 +2234,7 @@ public String toString() { } @JsonTypeName("u27") + @JsonIgnoreProperties(ignoreUnknown = true) private static final class U27Wrapper implements Base { private final String value; @@ -2352,6 +2280,7 @@ public String toString() { } @JsonTypeName("u28") + @JsonIgnoreProperties(ignoreUnknown = true) private static final class U28Wrapper implements Base { private final String value; @@ -2397,6 +2326,7 @@ public String toString() { } @JsonTypeName("u29") + @JsonIgnoreProperties(ignoreUnknown = true) private static final class U29Wrapper implements Base { private final String value; @@ -2442,6 +2372,7 @@ public String toString() { } @JsonTypeName("u30") + @JsonIgnoreProperties(ignoreUnknown = true) private static final class U30Wrapper implements Base { private final String value; @@ -2487,6 +2418,7 @@ public String toString() { } @JsonTypeName("u31") + @JsonIgnoreProperties(ignoreUnknown = true) private static final class U31Wrapper implements Base { private final String value; @@ -2532,6 +2464,7 @@ public String toString() { } @JsonTypeName("u32") + @JsonIgnoreProperties(ignoreUnknown = true) private static final class U32Wrapper implements Base { private final String value; @@ -2577,6 +2510,7 @@ public String toString() { } @JsonTypeName("u33") + @JsonIgnoreProperties(ignoreUnknown = true) private static final class U33Wrapper implements Base { private final String value; @@ -2622,6 +2556,7 @@ public String toString() { } @JsonTypeName("u34") + @JsonIgnoreProperties(ignoreUnknown = true) private static final class U34Wrapper implements Base { private final String value; @@ -2667,6 +2602,7 @@ public String toString() { } @JsonTypeName("u35") + @JsonIgnoreProperties(ignoreUnknown = true) private static final class U35Wrapper implements Base { private final String value; @@ -2712,6 +2648,7 @@ public String toString() { } @JsonTypeName("u36") + @JsonIgnoreProperties(ignoreUnknown = true) private static final class U36Wrapper implements Base { private final String value; @@ -2757,6 +2694,7 @@ public String toString() { } @JsonTypeName("u37") + @JsonIgnoreProperties(ignoreUnknown = true) private static final class U37Wrapper implements Base { private final String value; @@ -2802,6 +2740,7 @@ public String toString() { } @JsonTypeName("u38") + @JsonIgnoreProperties(ignoreUnknown = true) private static final class U38Wrapper implements Base { private final String value; @@ -2847,6 +2786,7 @@ public String toString() { } @JsonTypeName("u39") + @JsonIgnoreProperties(ignoreUnknown = true) private static final class U39Wrapper implements Base { private final String value; @@ -2892,6 +2832,7 @@ public String toString() { } @JsonTypeName("u40") + @JsonIgnoreProperties(ignoreUnknown = true) private static final class U40Wrapper implements Base { private final String value; @@ -2937,6 +2878,7 @@ public String toString() { } @JsonTypeName("u41") + @JsonIgnoreProperties(ignoreUnknown = true) private static final class U41Wrapper implements Base { private final String value; @@ -2982,6 +2924,7 @@ public String toString() { } @JsonTypeName("u42") + @JsonIgnoreProperties(ignoreUnknown = true) private static final class U42Wrapper implements Base { private final String value; @@ -3027,6 +2970,7 @@ public String toString() { } @JsonTypeName("u43") + @JsonIgnoreProperties(ignoreUnknown = true) private static final class U43Wrapper implements Base { private final String value; @@ -3072,6 +3016,7 @@ public String toString() { } @JsonTypeName("u44") + @JsonIgnoreProperties(ignoreUnknown = true) private static final class U44Wrapper implements Base { private final String value; @@ -3117,6 +3062,7 @@ public String toString() { } @JsonTypeName("u45") + @JsonIgnoreProperties(ignoreUnknown = true) private static final class U45Wrapper implements Base { private final String value; @@ -3162,6 +3108,7 @@ public String toString() { } @JsonTypeName("u46") + @JsonIgnoreProperties(ignoreUnknown = true) private static final class U46Wrapper implements Base { private final String value; @@ -3207,6 +3154,7 @@ public String toString() { } @JsonTypeName("u47") + @JsonIgnoreProperties(ignoreUnknown = true) private static final class U47Wrapper implements Base { private final String value; @@ -3252,6 +3200,7 @@ public String toString() { } @JsonTypeName("u48") + @JsonIgnoreProperties(ignoreUnknown = true) private static final class U48Wrapper implements Base { private final String value; @@ -3297,6 +3246,7 @@ public String toString() { } @JsonTypeName("u49") + @JsonIgnoreProperties(ignoreUnknown = true) private static final class U49Wrapper implements Base { private final String value; @@ -3342,6 +3292,7 @@ public String toString() { } @JsonTypeName("u50") + @JsonIgnoreProperties(ignoreUnknown = true) private static final class U50Wrapper implements Base { private final String value; @@ -3387,6 +3338,7 @@ public String toString() { } @JsonTypeName("u51") + @JsonIgnoreProperties(ignoreUnknown = true) private static final class U51Wrapper implements Base { private final String value; @@ -3432,6 +3384,7 @@ public String toString() { } @JsonTypeName("u52") + @JsonIgnoreProperties(ignoreUnknown = true) private static final class U52Wrapper implements Base { private final String value; @@ -3477,6 +3430,7 @@ public String toString() { } @JsonTypeName("u53") + @JsonIgnoreProperties(ignoreUnknown = true) private static final class U53Wrapper implements Base { private final String value; @@ -3522,6 +3476,7 @@ public String toString() { } @JsonTypeName("u54") + @JsonIgnoreProperties(ignoreUnknown = true) private static final class U54Wrapper implements Base { private final String value; @@ -3567,6 +3522,7 @@ public String toString() { } @JsonTypeName("u55") + @JsonIgnoreProperties(ignoreUnknown = true) private static final class U55Wrapper implements Base { private final String value; @@ -3612,6 +3568,7 @@ public String toString() { } @JsonTypeName("u56") + @JsonIgnoreProperties(ignoreUnknown = true) private static final class U56Wrapper implements Base { private final String value; @@ -3657,6 +3614,7 @@ public String toString() { } @JsonTypeName("u57") + @JsonIgnoreProperties(ignoreUnknown = true) private static final class U57Wrapper implements Base { private final String value; @@ -3702,6 +3660,7 @@ public String toString() { } @JsonTypeName("u58") + @JsonIgnoreProperties(ignoreUnknown = true) private static final class U58Wrapper implements Base { private final String value; @@ -3747,6 +3706,7 @@ public String toString() { } @JsonTypeName("u59") + @JsonIgnoreProperties(ignoreUnknown = true) private static final class U59Wrapper implements Base { private final String value; @@ -3792,6 +3752,7 @@ public String toString() { } @JsonTypeName("u60") + @JsonIgnoreProperties(ignoreUnknown = true) private static final class U60Wrapper implements Base { private final String value; @@ -3837,6 +3798,7 @@ public String toString() { } @JsonTypeName("u61") + @JsonIgnoreProperties(ignoreUnknown = true) private static final class U61Wrapper implements Base { private final String value; @@ -3882,6 +3844,7 @@ public String toString() { } @JsonTypeName("u62") + @JsonIgnoreProperties(ignoreUnknown = true) private static final class U62Wrapper implements Base { private final String value; @@ -3927,6 +3890,7 @@ public String toString() { } @JsonTypeName("u63") + @JsonIgnoreProperties(ignoreUnknown = true) private static final class U63Wrapper implements Base { private final String value; @@ -3972,6 +3936,7 @@ public String toString() { } @JsonTypeName("u64") + @JsonIgnoreProperties(ignoreUnknown = true) private static final class U64Wrapper implements Base { private final String value; @@ -4017,6 +3982,7 @@ public String toString() { } @JsonTypeName("u65") + @JsonIgnoreProperties(ignoreUnknown = true) private static final class U65Wrapper implements Base { private final String value; @@ -4062,6 +4028,7 @@ public String toString() { } @JsonTypeName("u66") + @JsonIgnoreProperties(ignoreUnknown = true) private static final class U66Wrapper implements Base { private final String value; @@ -4107,6 +4074,7 @@ public String toString() { } @JsonTypeName("u67") + @JsonIgnoreProperties(ignoreUnknown = true) private static final class U67Wrapper implements Base { private final String value; @@ -4152,6 +4120,7 @@ public String toString() { } @JsonTypeName("u68") + @JsonIgnoreProperties(ignoreUnknown = true) private static final class U68Wrapper implements Base { private final String value; @@ -4197,6 +4166,7 @@ public String toString() { } @JsonTypeName("u69") + @JsonIgnoreProperties(ignoreUnknown = true) private static final class U69Wrapper implements Base { private final String value; @@ -4242,6 +4212,7 @@ public String toString() { } @JsonTypeName("u70") + @JsonIgnoreProperties(ignoreUnknown = true) private static final class U70Wrapper implements Base { private final String value; @@ -4287,6 +4258,7 @@ public String toString() { } @JsonTypeName("u71") + @JsonIgnoreProperties(ignoreUnknown = true) private static final class U71Wrapper implements Base { private final String value; @@ -4332,6 +4304,7 @@ public String toString() { } @JsonTypeName("u72") + @JsonIgnoreProperties(ignoreUnknown = true) private static final class U72Wrapper implements Base { private final String value; @@ -4377,6 +4350,7 @@ public String toString() { } @JsonTypeName("u73") + @JsonIgnoreProperties(ignoreUnknown = true) private static final class U73Wrapper implements Base { private final String value; @@ -4422,6 +4396,7 @@ public String toString() { } @JsonTypeName("u74") + @JsonIgnoreProperties(ignoreUnknown = true) private static final class U74Wrapper implements Base { private final String value; @@ -4467,6 +4442,7 @@ public String toString() { } @JsonTypeName("u75") + @JsonIgnoreProperties(ignoreUnknown = true) private static final class U75Wrapper implements Base { private final String value; @@ -4512,6 +4488,7 @@ public String toString() { } @JsonTypeName("u76") + @JsonIgnoreProperties(ignoreUnknown = true) private static final class U76Wrapper implements Base { private final String value; @@ -4557,6 +4534,7 @@ public String toString() { } @JsonTypeName("u77") + @JsonIgnoreProperties(ignoreUnknown = true) private static final class U77Wrapper implements Base { private final String value; @@ -4602,6 +4580,7 @@ public String toString() { } @JsonTypeName("u78") + @JsonIgnoreProperties(ignoreUnknown = true) private static final class U78Wrapper implements Base { private final String value; @@ -4647,6 +4626,7 @@ public String toString() { } @JsonTypeName("u79") + @JsonIgnoreProperties(ignoreUnknown = true) private static final class U79Wrapper implements Base { private final String value; @@ -4692,6 +4672,7 @@ public String toString() { } @JsonTypeName("u80") + @JsonIgnoreProperties(ignoreUnknown = true) private static final class U80Wrapper implements Base { private final String value; @@ -4737,6 +4718,7 @@ public String toString() { } @JsonTypeName("u81") + @JsonIgnoreProperties(ignoreUnknown = true) private static final class U81Wrapper implements Base { private final String value; @@ -4782,6 +4764,7 @@ public String toString() { } @JsonTypeName("u82") + @JsonIgnoreProperties(ignoreUnknown = true) private static final class U82Wrapper implements Base { private final String value; @@ -4827,6 +4810,7 @@ public String toString() { } @JsonTypeName("u83") + @JsonIgnoreProperties(ignoreUnknown = true) private static final class U83Wrapper implements Base { private final String value; @@ -4872,6 +4856,7 @@ public String toString() { } @JsonTypeName("u84") + @JsonIgnoreProperties(ignoreUnknown = true) private static final class U84Wrapper implements Base { private final String value; @@ -4917,6 +4902,7 @@ public String toString() { } @JsonTypeName("u85") + @JsonIgnoreProperties(ignoreUnknown = true) private static final class U85Wrapper implements Base { private final String value; @@ -4962,6 +4948,7 @@ public String toString() { } @JsonTypeName("u86") + @JsonIgnoreProperties(ignoreUnknown = true) private static final class U86Wrapper implements Base { private final String value; @@ -5007,6 +4994,7 @@ public String toString() { } @JsonTypeName("u87") + @JsonIgnoreProperties(ignoreUnknown = true) private static final class U87Wrapper implements Base { private final String value; @@ -5052,6 +5040,7 @@ public String toString() { } @JsonTypeName("u88") + @JsonIgnoreProperties(ignoreUnknown = true) private static final class U88Wrapper implements Base { private final String value; @@ -5097,6 +5086,7 @@ public String toString() { } @JsonTypeName("u89") + @JsonIgnoreProperties(ignoreUnknown = true) private static final class U89Wrapper implements Base { private final String value; @@ -5142,6 +5132,7 @@ public String toString() { } @JsonTypeName("u90") + @JsonIgnoreProperties(ignoreUnknown = true) private static final class U90Wrapper implements Base { private final String value; @@ -5187,6 +5178,7 @@ public String toString() { } @JsonTypeName("u91") + @JsonIgnoreProperties(ignoreUnknown = true) private static final class U91Wrapper implements Base { private final String value; @@ -5232,6 +5224,7 @@ public String toString() { } @JsonTypeName("u92") + @JsonIgnoreProperties(ignoreUnknown = true) private static final class U92Wrapper implements Base { private final String value; @@ -5277,6 +5270,7 @@ public String toString() { } @JsonTypeName("u93") + @JsonIgnoreProperties(ignoreUnknown = true) private static final class U93Wrapper implements Base { private final String value; @@ -5322,6 +5316,7 @@ public String toString() { } @JsonTypeName("u94") + @JsonIgnoreProperties(ignoreUnknown = true) private static final class U94Wrapper implements Base { private final String value; @@ -5367,6 +5362,7 @@ public String toString() { } @JsonTypeName("u95") + @JsonIgnoreProperties(ignoreUnknown = true) private static final class U95Wrapper implements Base { private final String value; @@ -5412,6 +5408,7 @@ public String toString() { } @JsonTypeName("u96") + @JsonIgnoreProperties(ignoreUnknown = true) private static final class U96Wrapper implements Base { private final String value; @@ -5457,6 +5454,7 @@ public String toString() { } @JsonTypeName("u97") + @JsonIgnoreProperties(ignoreUnknown = true) private static final class U97Wrapper implements Base { private final String value; @@ -5502,6 +5500,7 @@ public String toString() { } @JsonTypeName("u98") + @JsonIgnoreProperties(ignoreUnknown = true) private static final class U98Wrapper implements Base { private final String value; @@ -5547,6 +5546,7 @@ public String toString() { } @JsonTypeName("u99") + @JsonIgnoreProperties(ignoreUnknown = true) private static final class U99Wrapper implements Base { private final String value; @@ -5592,6 +5592,7 @@ public String toString() { } @JsonTypeName("u100") + @JsonIgnoreProperties(ignoreUnknown = true) private static final class U100Wrapper implements Base { private final String value; @@ -5695,4 +5696,329 @@ public String toString() { return "UnknownWrapper{type: " + type + ", value: " + value + '}'; } } + + static final class Deserializer extends JsonDeserializer implements ResolvableDeserializer { + private static final Class[] VARIANT_TYPES = new Class[] { + U0Wrapper.class, + U1Wrapper.class, + U2Wrapper.class, + U3Wrapper.class, + U4Wrapper.class, + U5Wrapper.class, + U6Wrapper.class, + U7Wrapper.class, + U8Wrapper.class, + U9Wrapper.class, + U10Wrapper.class, + U11Wrapper.class, + U12Wrapper.class, + U13Wrapper.class, + U14Wrapper.class, + U15Wrapper.class, + U16Wrapper.class, + U17Wrapper.class, + U18Wrapper.class, + U19Wrapper.class, + U20Wrapper.class, + U21Wrapper.class, + U22Wrapper.class, + U23Wrapper.class, + U24Wrapper.class, + U25Wrapper.class, + U26Wrapper.class, + U27Wrapper.class, + U28Wrapper.class, + U29Wrapper.class, + U30Wrapper.class, + U31Wrapper.class, + U32Wrapper.class, + U33Wrapper.class, + U34Wrapper.class, + U35Wrapper.class, + U36Wrapper.class, + U37Wrapper.class, + U38Wrapper.class, + U39Wrapper.class, + U40Wrapper.class, + U41Wrapper.class, + U42Wrapper.class, + U43Wrapper.class, + U44Wrapper.class, + U45Wrapper.class, + U46Wrapper.class, + U47Wrapper.class, + U48Wrapper.class, + U49Wrapper.class, + U50Wrapper.class, + U51Wrapper.class, + U52Wrapper.class, + U53Wrapper.class, + U54Wrapper.class, + U55Wrapper.class, + U56Wrapper.class, + U57Wrapper.class, + U58Wrapper.class, + U59Wrapper.class, + U60Wrapper.class, + U61Wrapper.class, + U62Wrapper.class, + U63Wrapper.class, + U64Wrapper.class, + U65Wrapper.class, + U66Wrapper.class, + U67Wrapper.class, + U68Wrapper.class, + U69Wrapper.class, + U70Wrapper.class, + U71Wrapper.class, + U72Wrapper.class, + U73Wrapper.class, + U74Wrapper.class, + U75Wrapper.class, + U76Wrapper.class, + U77Wrapper.class, + U78Wrapper.class, + U79Wrapper.class, + U80Wrapper.class, + U81Wrapper.class, + U82Wrapper.class, + U83Wrapper.class, + U84Wrapper.class, + U85Wrapper.class, + U86Wrapper.class, + U87Wrapper.class, + U88Wrapper.class, + U89Wrapper.class, + U90Wrapper.class, + U91Wrapper.class, + U92Wrapper.class, + U93Wrapper.class, + U94Wrapper.class, + U95Wrapper.class, + U96Wrapper.class, + U97Wrapper.class, + U98Wrapper.class, + U99Wrapper.class, + U100Wrapper.class + }; + + private volatile JsonDeserializer[] deserializers; + + @Override + public void resolve(DeserializationContext context) throws JsonMappingException { + deserializers = new JsonDeserializer[VARIANT_TYPES.length]; + } + + @Override + public boolean isCachable() { + return true; + } + + @Override + public LargeUnionExample deserialize(JsonParser parser, DeserializationContext context) throws IOException { + if (!parser.isExpectedStartObjectToken()) { + return context.reportInputMismatch( + LargeUnionExample.class, "Expected a JSON object for union deserialization"); + } + JsonToken firstToken = parser.nextToken(); + if (firstToken == JsonToken.FIELD_NAME && isTypeField(parser.currentName(), context)) { + if (parser.nextToken() != JsonToken.VALUE_STRING) { + return context.reportInputMismatch( + LargeUnionExample.class, "Union discriminator 'type' must be a string"); + } + String type = parser.getText(); + parser.nextToken(); + return deserializeSelected(parser, context, type); + } + return deserializeBuffered(parser, context); + } + + private LargeUnionExample deserializeBuffered(JsonParser parser, DeserializationContext context) + throws IOException { + try (TokenBuffer buffer = context.bufferForInputBuffering(parser)) { + buffer.writeStartObject(); + JsonToken token = parser.currentToken(); + while (token == JsonToken.FIELD_NAME) { + String fieldName = parser.currentName(); + JsonToken valueToken = parser.nextToken(); + if (isTypeField(fieldName, context)) { + if (valueToken != JsonToken.VALUE_STRING) { + return context.reportInputMismatch( + LargeUnionExample.class, "Union discriminator 'type' must be a string"); + } + String type = parser.getText(); + parser.nextToken(); + try (JsonParser bufferedParser = buffer.asParser(parser)) { + JsonParser combinedParser = + JsonParserSequence.createFlattened(true, bufferedParser, parser); + combinedParser.nextToken(); + return deserializeSelected(combinedParser, context, type); + } + } + buffer.writeFieldName(fieldName); + buffer.copyCurrentStructure(parser); + token = parser.nextToken(); + } + if (token != JsonToken.END_OBJECT) { + return context.reportInputMismatch( + LargeUnionExample.class, "Expected the end of a JSON object while deserializing a union"); + } + } + return context.reportInputMismatch(LargeUnionExample.class, "Union discriminator 'type' is required"); + } + + private static boolean isTypeField(String fieldName, DeserializationContext context) { + return "type".equals(fieldName) + || (context.isEnabled(MapperFeature.ACCEPT_CASE_INSENSITIVE_PROPERTIES) + && "type".equalsIgnoreCase(fieldName)); + } + + private LargeUnionExample deserializeSelected(JsonParser parser, DeserializationContext context, String type) + throws IOException { + int variantIndex = + switch (type) { + case "u0" -> 0; + case "u1" -> 1; + case "u2" -> 2; + case "u3" -> 3; + case "u4" -> 4; + case "u5" -> 5; + case "u6" -> 6; + case "u7" -> 7; + case "u8" -> 8; + case "u9" -> 9; + case "u10" -> 10; + case "u11" -> 11; + case "u12" -> 12; + case "u13" -> 13; + case "u14" -> 14; + case "u15" -> 15; + case "u16" -> 16; + case "u17" -> 17; + case "u18" -> 18; + case "u19" -> 19; + case "u20" -> 20; + case "u21" -> 21; + case "u22" -> 22; + case "u23" -> 23; + case "u24" -> 24; + case "u25" -> 25; + case "u26" -> 26; + case "u27" -> 27; + case "u28" -> 28; + case "u29" -> 29; + case "u30" -> 30; + case "u31" -> 31; + case "u32" -> 32; + case "u33" -> 33; + case "u34" -> 34; + case "u35" -> 35; + case "u36" -> 36; + case "u37" -> 37; + case "u38" -> 38; + case "u39" -> 39; + case "u40" -> 40; + case "u41" -> 41; + case "u42" -> 42; + case "u43" -> 43; + case "u44" -> 44; + case "u45" -> 45; + case "u46" -> 46; + case "u47" -> 47; + case "u48" -> 48; + case "u49" -> 49; + case "u50" -> 50; + case "u51" -> 51; + case "u52" -> 52; + case "u53" -> 53; + case "u54" -> 54; + case "u55" -> 55; + case "u56" -> 56; + case "u57" -> 57; + case "u58" -> 58; + case "u59" -> 59; + case "u60" -> 60; + case "u61" -> 61; + case "u62" -> 62; + case "u63" -> 63; + case "u64" -> 64; + case "u65" -> 65; + case "u66" -> 66; + case "u67" -> 67; + case "u68" -> 68; + case "u69" -> 69; + case "u70" -> 70; + case "u71" -> 71; + case "u72" -> 72; + case "u73" -> 73; + case "u74" -> 74; + case "u75" -> 75; + case "u76" -> 76; + case "u77" -> 77; + case "u78" -> 78; + case "u79" -> 79; + case "u80" -> 80; + case "u81" -> 81; + case "u82" -> 82; + case "u83" -> 83; + case "u84" -> 84; + case "u85" -> 85; + case "u86" -> 86; + case "u87" -> 87; + case "u88" -> 88; + case "u89" -> 89; + case "u90" -> 90; + case "u91" -> 91; + case "u92" -> 92; + case "u93" -> 93; + case "u94" -> 94; + case "u95" -> 95; + case "u96" -> 96; + case "u97" -> 97; + case "u98" -> 98; + case "u99" -> 99; + case "u100" -> 100; + default -> -1; + }; + if (variantIndex < 0) { + return deserializeUnknown(parser, context, type); + } + JsonDeserializer deserializer = deserializers[variantIndex]; + if (deserializer == null) { + deserializer = resolveDeserializer(context, variantIndex); + } + return new LargeUnionExample((Base) deserializer.deserialize(parser, context)); + } + + private synchronized JsonDeserializer resolveDeserializer(DeserializationContext context, int variantIndex) + throws JsonMappingException { + JsonDeserializer deserializer = deserializers[variantIndex]; + if (deserializer == null) { + deserializer = context.findRootValueDeserializer(context.constructType(VARIANT_TYPES[variantIndex])); + JsonDeserializer[] updated = deserializers.clone(); + updated[variantIndex] = deserializer; + deserializers = updated; + } + return deserializer; + } + + private static LargeUnionExample deserializeUnknown( + JsonParser parser, DeserializationContext context, String type) throws IOException { + Map values = new HashMap<>(); + if (parser.currentToken() == JsonToken.START_OBJECT) { + parser.nextToken(); + } + while (parser.currentToken() == JsonToken.FIELD_NAME) { + String fieldName = parser.currentName(); + parser.nextToken(); + values.put(fieldName, context.readValue(parser, Object.class)); + parser.nextToken(); + } + if (parser.currentToken() != JsonToken.END_OBJECT) { + return context.reportInputMismatch( + LargeUnionExample.class, "Expected the end of a JSON object while deserializing a union"); + } + return new LargeUnionExample(new UnknownWrapper(type, values)); + } + } } diff --git a/conjure-java-core/src/integrationInput/java/allexamples/com/palantir/product/ManyFieldExample.java b/conjure-java-core/src/integrationInput/java/allexamples/com/palantir/product/ManyFieldExample.java index f55f549c3..f5222f968 100644 --- a/conjure-java-core/src/integrationInput/java/allexamples/com/palantir/product/ManyFieldExample.java +++ b/conjure-java-core/src/integrationInput/java/allexamples/com/palantir/product/ManyFieldExample.java @@ -220,6 +220,7 @@ public static final class Builder { private Set set = ConjureCollections.newNonNullSet(); + @JsonSetter(value = "map", nulls = Nulls.SKIP, contentNulls = Nulls.FAIL) private Map map = new LinkedHashMap<>(); private StringAliasExample alias; @@ -343,7 +344,6 @@ public Builder set(String set) { /** @deprecated deprecation documentation. */ @Deprecated - @JsonSetter(value = "map", nulls = Nulls.SKIP, contentNulls = Nulls.FAIL) public Builder map(@Nonnull Map map) { checkNotBuilt(); this.map = new LinkedHashMap<>(Preconditions.checkNotNull(map, "map cannot be null")); diff --git a/conjure-java-core/src/integrationInput/java/allexamples/com/palantir/product/MapExample.java b/conjure-java-core/src/integrationInput/java/allexamples/com/palantir/product/MapExample.java index 5a88a5b57..b33f597fd 100644 --- a/conjure-java-core/src/integrationInput/java/allexamples/com/palantir/product/MapExample.java +++ b/conjure-java-core/src/integrationInput/java/allexamples/com/palantir/product/MapExample.java @@ -134,10 +134,13 @@ public static Builder builder() { public static final class Builder { boolean _buildInvoked; + @JsonSetter(value = "items", nulls = Nulls.SKIP, contentNulls = Nulls.FAIL) private Map items = new LinkedHashMap<>(); + @JsonSetter(value = "optionalItems", nulls = Nulls.SKIP, contentNulls = Nulls.AS_EMPTY) private Map> optionalItems = new LinkedHashMap<>(); + @JsonSetter(value = "aliasOptionalItems", nulls = Nulls.SKIP, contentNulls = Nulls.AS_EMPTY) private Map aliasOptionalItems = new LinkedHashMap<>(); private Builder() {} @@ -150,7 +153,6 @@ public Builder from(MapExample other) { return this; } - @JsonSetter(value = "items", nulls = Nulls.SKIP, contentNulls = Nulls.FAIL) public Builder items(@Nonnull Map items) { checkNotBuilt(); this.items = new LinkedHashMap<>(Preconditions.checkNotNull(items, "items cannot be null")); @@ -169,7 +171,6 @@ public Builder items(String key, String value) { return this; } - @JsonSetter(value = "optionalItems", nulls = Nulls.SKIP, contentNulls = Nulls.AS_EMPTY) public Builder optionalItems(@Nonnull Map> optionalItems) { checkNotBuilt(); this.optionalItems = @@ -189,7 +190,6 @@ public Builder optionalItems(String key, Optional value) { return this; } - @JsonSetter(value = "aliasOptionalItems", nulls = Nulls.SKIP, contentNulls = Nulls.AS_EMPTY) public Builder aliasOptionalItems(@Nonnull Map aliasOptionalItems) { checkNotBuilt(); this.aliasOptionalItems = new LinkedHashMap<>( diff --git a/conjure-java-core/src/integrationInput/java/allexamples/com/palantir/product/SingleUnion.java b/conjure-java-core/src/integrationInput/java/allexamples/com/palantir/product/SingleUnion.java index d4b7cc81a..3cb47a64c 100644 --- a/conjure-java-core/src/integrationInput/java/allexamples/com/palantir/product/SingleUnion.java +++ b/conjure-java-core/src/integrationInput/java/allexamples/com/palantir/product/SingleUnion.java @@ -6,14 +6,23 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.annotation.JsonSetter; -import com.fasterxml.jackson.annotation.JsonSubTypes; -import com.fasterxml.jackson.annotation.JsonTypeInfo; import com.fasterxml.jackson.annotation.JsonTypeName; import com.fasterxml.jackson.annotation.JsonValue; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.JsonToken; +import com.fasterxml.jackson.core.util.JsonParserSequence; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonDeserializer; +import com.fasterxml.jackson.databind.JsonMappingException; +import com.fasterxml.jackson.databind.MapperFeature; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.deser.ResolvableDeserializer; +import com.fasterxml.jackson.databind.util.TokenBuffer; import com.palantir.logsafe.Preconditions; import com.palantir.logsafe.Safe; import com.palantir.logsafe.SafeArg; import com.palantir.logsafe.exceptions.SafeIllegalArgumentException; +import java.io.IOException; import java.util.Collections; import java.util.HashMap; import java.util.Map; @@ -24,6 +33,7 @@ import javax.annotation.processing.Generated; @Generated("com.palantir.conjure.java.types.UnionGenerator") +@JsonDeserialize(using = SingleUnion.Deserializer.class) public final class SingleUnion { private final Base value; @@ -154,19 +164,12 @@ public interface Completed_StageVisitorBuilder { Visitor build(); } - @JsonTypeInfo( - use = JsonTypeInfo.Id.NAME, - include = JsonTypeInfo.As.EXISTING_PROPERTY, - property = "type", - visible = true, - defaultImpl = UnknownWrapper.class) - @JsonSubTypes(@JsonSubTypes.Type(FooWrapper.class)) - @JsonIgnoreProperties(ignoreUnknown = true) private interface Base { T accept(Visitor visitor); } @JsonTypeName("foo") + @JsonIgnoreProperties(ignoreUnknown = true) private static final class FooWrapper implements Base { private final String value; @@ -270,4 +273,126 @@ public String toString() { return "UnknownWrapper{type: " + type + ", value: " + value + '}'; } } + + static final class Deserializer extends JsonDeserializer implements ResolvableDeserializer { + private static final Class[] VARIANT_TYPES = new Class[] {FooWrapper.class}; + + private volatile JsonDeserializer[] deserializers; + + @Override + public void resolve(DeserializationContext context) throws JsonMappingException { + deserializers = new JsonDeserializer[VARIANT_TYPES.length]; + } + + @Override + public boolean isCachable() { + return true; + } + + @Override + public SingleUnion deserialize(JsonParser parser, DeserializationContext context) throws IOException { + if (!parser.isExpectedStartObjectToken()) { + return context.reportInputMismatch( + SingleUnion.class, "Expected a JSON object for union deserialization"); + } + JsonToken firstToken = parser.nextToken(); + if (firstToken == JsonToken.FIELD_NAME && isTypeField(parser.currentName(), context)) { + if (parser.nextToken() != JsonToken.VALUE_STRING) { + return context.reportInputMismatch( + SingleUnion.class, "Union discriminator 'type' must be a string"); + } + String type = parser.getText(); + parser.nextToken(); + return deserializeSelected(parser, context, type); + } + return deserializeBuffered(parser, context); + } + + private SingleUnion deserializeBuffered(JsonParser parser, DeserializationContext context) throws IOException { + try (TokenBuffer buffer = context.bufferForInputBuffering(parser)) { + buffer.writeStartObject(); + JsonToken token = parser.currentToken(); + while (token == JsonToken.FIELD_NAME) { + String fieldName = parser.currentName(); + JsonToken valueToken = parser.nextToken(); + if (isTypeField(fieldName, context)) { + if (valueToken != JsonToken.VALUE_STRING) { + return context.reportInputMismatch( + SingleUnion.class, "Union discriminator 'type' must be a string"); + } + String type = parser.getText(); + parser.nextToken(); + try (JsonParser bufferedParser = buffer.asParser(parser)) { + JsonParser combinedParser = + JsonParserSequence.createFlattened(true, bufferedParser, parser); + combinedParser.nextToken(); + return deserializeSelected(combinedParser, context, type); + } + } + buffer.writeFieldName(fieldName); + buffer.copyCurrentStructure(parser); + token = parser.nextToken(); + } + if (token != JsonToken.END_OBJECT) { + return context.reportInputMismatch( + SingleUnion.class, "Expected the end of a JSON object while deserializing a union"); + } + } + return context.reportInputMismatch(SingleUnion.class, "Union discriminator 'type' is required"); + } + + private static boolean isTypeField(String fieldName, DeserializationContext context) { + return "type".equals(fieldName) + || (context.isEnabled(MapperFeature.ACCEPT_CASE_INSENSITIVE_PROPERTIES) + && "type".equalsIgnoreCase(fieldName)); + } + + private SingleUnion deserializeSelected(JsonParser parser, DeserializationContext context, String type) + throws IOException { + int variantIndex = + switch (type) { + case "foo" -> 0; + default -> -1; + }; + if (variantIndex < 0) { + return deserializeUnknown(parser, context, type); + } + JsonDeserializer deserializer = deserializers[variantIndex]; + if (deserializer == null) { + deserializer = resolveDeserializer(context, variantIndex); + } + return new SingleUnion((Base) deserializer.deserialize(parser, context)); + } + + private synchronized JsonDeserializer resolveDeserializer(DeserializationContext context, int variantIndex) + throws JsonMappingException { + JsonDeserializer deserializer = deserializers[variantIndex]; + if (deserializer == null) { + deserializer = context.findRootValueDeserializer(context.constructType(VARIANT_TYPES[variantIndex])); + JsonDeserializer[] updated = deserializers.clone(); + updated[variantIndex] = deserializer; + deserializers = updated; + } + return deserializer; + } + + private static SingleUnion deserializeUnknown(JsonParser parser, DeserializationContext context, String type) + throws IOException { + Map values = new HashMap<>(); + if (parser.currentToken() == JsonToken.START_OBJECT) { + parser.nextToken(); + } + while (parser.currentToken() == JsonToken.FIELD_NAME) { + String fieldName = parser.currentName(); + parser.nextToken(); + values.put(fieldName, context.readValue(parser, Object.class)); + parser.nextToken(); + } + if (parser.currentToken() != JsonToken.END_OBJECT) { + return context.reportInputMismatch( + SingleUnion.class, "Expected the end of a JSON object while deserializing a union"); + } + return new SingleUnion(new UnknownWrapper(type, values)); + } + } } diff --git a/conjure-java-core/src/integrationInput/java/allexamples/com/palantir/product/Union.java b/conjure-java-core/src/integrationInput/java/allexamples/com/palantir/product/Union.java index 56af03ca9..2cf0aadfb 100644 --- a/conjure-java-core/src/integrationInput/java/allexamples/com/palantir/product/Union.java +++ b/conjure-java-core/src/integrationInput/java/allexamples/com/palantir/product/Union.java @@ -6,14 +6,23 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.annotation.JsonSetter; -import com.fasterxml.jackson.annotation.JsonSubTypes; -import com.fasterxml.jackson.annotation.JsonTypeInfo; import com.fasterxml.jackson.annotation.JsonTypeName; import com.fasterxml.jackson.annotation.JsonValue; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.JsonToken; +import com.fasterxml.jackson.core.util.JsonParserSequence; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonDeserializer; +import com.fasterxml.jackson.databind.JsonMappingException; +import com.fasterxml.jackson.databind.MapperFeature; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.deser.ResolvableDeserializer; +import com.fasterxml.jackson.databind.util.TokenBuffer; import com.palantir.logsafe.Preconditions; import com.palantir.logsafe.Safe; import com.palantir.logsafe.SafeArg; import com.palantir.logsafe.exceptions.SafeIllegalArgumentException; +import java.io.IOException; import java.util.Collections; import java.util.HashMap; import java.util.Map; @@ -25,6 +34,7 @@ import javax.annotation.processing.Generated; @Generated("com.palantir.conjure.java.types.UnionGenerator") +@JsonDeserialize(using = Union.Deserializer.class) public final class Union { private final Base value; @@ -231,23 +241,12 @@ public interface Completed_StageVisitorBuilder { Visitor build(); } - @JsonTypeInfo( - use = JsonTypeInfo.Id.NAME, - include = JsonTypeInfo.As.EXISTING_PROPERTY, - property = "type", - visible = true, - defaultImpl = UnknownWrapper.class) - @JsonSubTypes({ - @JsonSubTypes.Type(FooWrapper.class), - @JsonSubTypes.Type(BarWrapper.class), - @JsonSubTypes.Type(BazWrapper.class) - }) - @JsonIgnoreProperties(ignoreUnknown = true) private interface Base { T accept(Visitor visitor); } @JsonTypeName("foo") + @JsonIgnoreProperties(ignoreUnknown = true) private static final class FooWrapper implements Base { private final String value; @@ -293,6 +292,7 @@ public String toString() { } @JsonTypeName("bar") + @JsonIgnoreProperties(ignoreUnknown = true) private static final class BarWrapper implements Base { private final int value; @@ -339,6 +339,7 @@ public String toString() { } @JsonTypeName("baz") + @JsonIgnoreProperties(ignoreUnknown = true) private static final class BazWrapper implements Base { private final long value; @@ -443,4 +444,127 @@ public String toString() { return "UnknownWrapper{type: " + type + ", value: " + value + '}'; } } + + static final class Deserializer extends JsonDeserializer implements ResolvableDeserializer { + private static final Class[] VARIANT_TYPES = + new Class[] {FooWrapper.class, BarWrapper.class, BazWrapper.class}; + + private volatile JsonDeserializer[] deserializers; + + @Override + public void resolve(DeserializationContext context) throws JsonMappingException { + deserializers = new JsonDeserializer[VARIANT_TYPES.length]; + } + + @Override + public boolean isCachable() { + return true; + } + + @Override + public Union deserialize(JsonParser parser, DeserializationContext context) throws IOException { + if (!parser.isExpectedStartObjectToken()) { + return context.reportInputMismatch(Union.class, "Expected a JSON object for union deserialization"); + } + JsonToken firstToken = parser.nextToken(); + if (firstToken == JsonToken.FIELD_NAME && isTypeField(parser.currentName(), context)) { + if (parser.nextToken() != JsonToken.VALUE_STRING) { + return context.reportInputMismatch(Union.class, "Union discriminator 'type' must be a string"); + } + String type = parser.getText(); + parser.nextToken(); + return deserializeSelected(parser, context, type); + } + return deserializeBuffered(parser, context); + } + + private Union deserializeBuffered(JsonParser parser, DeserializationContext context) throws IOException { + try (TokenBuffer buffer = context.bufferForInputBuffering(parser)) { + buffer.writeStartObject(); + JsonToken token = parser.currentToken(); + while (token == JsonToken.FIELD_NAME) { + String fieldName = parser.currentName(); + JsonToken valueToken = parser.nextToken(); + if (isTypeField(fieldName, context)) { + if (valueToken != JsonToken.VALUE_STRING) { + return context.reportInputMismatch( + Union.class, "Union discriminator 'type' must be a string"); + } + String type = parser.getText(); + parser.nextToken(); + try (JsonParser bufferedParser = buffer.asParser(parser)) { + JsonParser combinedParser = + JsonParserSequence.createFlattened(true, bufferedParser, parser); + combinedParser.nextToken(); + return deserializeSelected(combinedParser, context, type); + } + } + buffer.writeFieldName(fieldName); + buffer.copyCurrentStructure(parser); + token = parser.nextToken(); + } + if (token != JsonToken.END_OBJECT) { + return context.reportInputMismatch( + Union.class, "Expected the end of a JSON object while deserializing a union"); + } + } + return context.reportInputMismatch(Union.class, "Union discriminator 'type' is required"); + } + + private static boolean isTypeField(String fieldName, DeserializationContext context) { + return "type".equals(fieldName) + || (context.isEnabled(MapperFeature.ACCEPT_CASE_INSENSITIVE_PROPERTIES) + && "type".equalsIgnoreCase(fieldName)); + } + + private Union deserializeSelected(JsonParser parser, DeserializationContext context, String type) + throws IOException { + int variantIndex = + switch (type) { + case "foo" -> 0; + case "bar" -> 1; + case "baz" -> 2; + default -> -1; + }; + if (variantIndex < 0) { + return deserializeUnknown(parser, context, type); + } + JsonDeserializer deserializer = deserializers[variantIndex]; + if (deserializer == null) { + deserializer = resolveDeserializer(context, variantIndex); + } + return new Union((Base) deserializer.deserialize(parser, context)); + } + + private synchronized JsonDeserializer resolveDeserializer(DeserializationContext context, int variantIndex) + throws JsonMappingException { + JsonDeserializer deserializer = deserializers[variantIndex]; + if (deserializer == null) { + deserializer = context.findRootValueDeserializer(context.constructType(VARIANT_TYPES[variantIndex])); + JsonDeserializer[] updated = deserializers.clone(); + updated[variantIndex] = deserializer; + deserializers = updated; + } + return deserializer; + } + + private static Union deserializeUnknown(JsonParser parser, DeserializationContext context, String type) + throws IOException { + Map values = new HashMap<>(); + if (parser.currentToken() == JsonToken.START_OBJECT) { + parser.nextToken(); + } + while (parser.currentToken() == JsonToken.FIELD_NAME) { + String fieldName = parser.currentName(); + parser.nextToken(); + values.put(fieldName, context.readValue(parser, Object.class)); + parser.nextToken(); + } + if (parser.currentToken() != JsonToken.END_OBJECT) { + return context.reportInputMismatch( + Union.class, "Expected the end of a JSON object while deserializing a union"); + } + return new Union(new UnknownWrapper(type, values)); + } + } } diff --git a/conjure-java-core/src/integrationInput/java/allexamples/com/palantir/product/UnionTypeExample.java b/conjure-java-core/src/integrationInput/java/allexamples/com/palantir/product/UnionTypeExample.java index b0d2f2300..ef70e4e86 100644 --- a/conjure-java-core/src/integrationInput/java/allexamples/com/palantir/product/UnionTypeExample.java +++ b/conjure-java-core/src/integrationInput/java/allexamples/com/palantir/product/UnionTypeExample.java @@ -6,17 +6,25 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.annotation.JsonSetter; -import com.fasterxml.jackson.annotation.JsonSubTypes; -import com.fasterxml.jackson.annotation.JsonTypeInfo; import com.fasterxml.jackson.annotation.JsonTypeName; import com.fasterxml.jackson.annotation.JsonValue; import com.fasterxml.jackson.annotation.Nulls; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.JsonToken; +import com.fasterxml.jackson.core.util.JsonParserSequence; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonDeserializer; +import com.fasterxml.jackson.databind.JsonMappingException; +import com.fasterxml.jackson.databind.MapperFeature; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.deser.ResolvableDeserializer; +import com.fasterxml.jackson.databind.util.TokenBuffer; import com.palantir.logsafe.Preconditions; import com.palantir.logsafe.Safe; import com.palantir.logsafe.SafeArg; import com.palantir.logsafe.Unsafe; import com.palantir.logsafe.exceptions.SafeIllegalArgumentException; +import java.io.IOException; import java.util.Collections; import java.util.HashMap; import java.util.LinkedHashSet; @@ -35,6 +43,7 @@ /** A type which can either be a StringExample, a set of strings, or an integer. */ @Unsafe @Generated("com.palantir.conjure.java.types.UnionGenerator") +@JsonDeserialize(using = UnionTypeExample.Deserializer.class) public final class UnionTypeExample { private final Base value; @@ -695,39 +704,12 @@ public interface Completed_StageVisitorBuilder { Visitor build(); } - @JsonTypeInfo( - use = JsonTypeInfo.Id.NAME, - include = JsonTypeInfo.As.EXISTING_PROPERTY, - property = "type", - visible = true, - defaultImpl = UnknownWrapper.class) - @JsonSubTypes({ - @JsonSubTypes.Type(StringExampleWrapper.class), - @JsonSubTypes.Type(ThisFieldIsAnIntegerWrapper.class), - @JsonSubTypes.Type(AlsoAnIntegerWrapper.class), - @JsonSubTypes.Type(IfWrapper.class), - @JsonSubTypes.Type(NewWrapper.class), - @JsonSubTypes.Type(InterfaceWrapper.class), - @JsonSubTypes.Type(CompletedWrapper.class), - @JsonSubTypes.Type(Unknown_Wrapper.class), - @JsonSubTypes.Type(OptionalWrapper.class), - @JsonSubTypes.Type(ListWrapper.class), - @JsonSubTypes.Type(SetWrapper.class), - @JsonSubTypes.Type(MapWrapper.class), - @JsonSubTypes.Type(OptionalAliasWrapper.class), - @JsonSubTypes.Type(ListAliasWrapper.class), - @JsonSubTypes.Type(SetAliasWrapper.class), - @JsonSubTypes.Type(MapAliasWrapper.class), - @JsonSubTypes.Type(BooleanFieldWrapper.class), - @JsonSubTypes.Type(SafeIntWrapper.class), - @JsonSubTypes.Type(UnsafeDoubleWrapper.class) - }) - @JsonIgnoreProperties(ignoreUnknown = true) private interface Base { T accept(Visitor visitor); } @JsonTypeName("stringExample") + @JsonIgnoreProperties(ignoreUnknown = true) private static final class StringExampleWrapper implements Base { private final StringExample value; @@ -773,6 +755,7 @@ public String toString() { } @JsonTypeName("thisFieldIsAnInteger") + @JsonIgnoreProperties(ignoreUnknown = true) private static final class ThisFieldIsAnIntegerWrapper implements Base { private final int value; @@ -819,6 +802,7 @@ public String toString() { } @JsonTypeName("alsoAnInteger") + @JsonIgnoreProperties(ignoreUnknown = true) private static final class AlsoAnIntegerWrapper implements Base { private final int value; @@ -864,6 +848,7 @@ public String toString() { } @JsonTypeName("if") + @JsonIgnoreProperties(ignoreUnknown = true) private static final class IfWrapper implements Base { private final int value; @@ -909,6 +894,7 @@ public String toString() { } @JsonTypeName("new") + @JsonIgnoreProperties(ignoreUnknown = true) private static final class NewWrapper implements Base { private final int value; @@ -954,6 +940,7 @@ public String toString() { } @JsonTypeName("interface") + @JsonIgnoreProperties(ignoreUnknown = true) private static final class InterfaceWrapper implements Base { private final int value; @@ -999,6 +986,7 @@ public String toString() { } @JsonTypeName("completed") + @JsonIgnoreProperties(ignoreUnknown = true) private static final class CompletedWrapper implements Base { private final int value; @@ -1044,6 +1032,7 @@ public String toString() { } @JsonTypeName("unknown") + @JsonIgnoreProperties(ignoreUnknown = true) private static final class Unknown_Wrapper implements Base { private final int value; @@ -1089,6 +1078,7 @@ public String toString() { } @JsonTypeName("optional") + @JsonIgnoreProperties(ignoreUnknown = true) private static final class OptionalWrapper implements Base { private final Optional value; @@ -1135,6 +1125,7 @@ public String toString() { } @JsonTypeName("list") + @JsonIgnoreProperties(ignoreUnknown = true) private static final class ListWrapper implements Base { private final List value; @@ -1180,6 +1171,7 @@ public String toString() { } @JsonTypeName("set") + @JsonIgnoreProperties(ignoreUnknown = true) private static final class SetWrapper implements Base { private final Set value; @@ -1227,6 +1219,7 @@ public String toString() { } @JsonTypeName("map") + @JsonIgnoreProperties(ignoreUnknown = true) private static final class MapWrapper implements Base { private final Map value; @@ -1272,6 +1265,7 @@ public String toString() { } @JsonTypeName("optionalAlias") + @JsonIgnoreProperties(ignoreUnknown = true) private static final class OptionalAliasWrapper implements Base { private final OptionalAlias value; @@ -1318,6 +1312,7 @@ public String toString() { } @JsonTypeName("listAlias") + @JsonIgnoreProperties(ignoreUnknown = true) private static final class ListAliasWrapper implements Base { private final ListAlias value; @@ -1363,6 +1358,7 @@ public String toString() { } @JsonTypeName("setAlias") + @JsonIgnoreProperties(ignoreUnknown = true) private static final class SetAliasWrapper implements Base { private final SetAlias value; @@ -1408,6 +1404,7 @@ public String toString() { } @JsonTypeName("mapAlias") + @JsonIgnoreProperties(ignoreUnknown = true) private static final class MapAliasWrapper implements Base { private final MapAliasExample value; @@ -1454,6 +1451,7 @@ public String toString() { } @JsonTypeName("booleanField") + @JsonIgnoreProperties(ignoreUnknown = true) private static final class BooleanFieldWrapper implements Base { private final boolean value; @@ -1499,6 +1497,7 @@ public String toString() { } @JsonTypeName("safeInt") + @JsonIgnoreProperties(ignoreUnknown = true) private static final class SafeIntWrapper implements Base { private final int value; @@ -1544,6 +1543,7 @@ public String toString() { } @JsonTypeName("unsafeDouble") + @JsonIgnoreProperties(ignoreUnknown = true) private static final class UnsafeDoubleWrapper implements Base { private final double value; @@ -1647,4 +1647,165 @@ public String toString() { return "UnknownWrapper{type: " + type + ", value: " + value + '}'; } } + + static final class Deserializer extends JsonDeserializer implements ResolvableDeserializer { + private static final Class[] VARIANT_TYPES = new Class[] { + StringExampleWrapper.class, + ThisFieldIsAnIntegerWrapper.class, + AlsoAnIntegerWrapper.class, + IfWrapper.class, + NewWrapper.class, + InterfaceWrapper.class, + CompletedWrapper.class, + Unknown_Wrapper.class, + OptionalWrapper.class, + ListWrapper.class, + SetWrapper.class, + MapWrapper.class, + OptionalAliasWrapper.class, + ListAliasWrapper.class, + SetAliasWrapper.class, + MapAliasWrapper.class, + BooleanFieldWrapper.class, + SafeIntWrapper.class, + UnsafeDoubleWrapper.class + }; + + private volatile JsonDeserializer[] deserializers; + + @Override + public void resolve(DeserializationContext context) throws JsonMappingException { + deserializers = new JsonDeserializer[VARIANT_TYPES.length]; + } + + @Override + public boolean isCachable() { + return true; + } + + @Override + public UnionTypeExample deserialize(JsonParser parser, DeserializationContext context) throws IOException { + if (!parser.isExpectedStartObjectToken()) { + return context.reportInputMismatch( + UnionTypeExample.class, "Expected a JSON object for union deserialization"); + } + JsonToken firstToken = parser.nextToken(); + if (firstToken == JsonToken.FIELD_NAME && isTypeField(parser.currentName(), context)) { + if (parser.nextToken() != JsonToken.VALUE_STRING) { + return context.reportInputMismatch( + UnionTypeExample.class, "Union discriminator 'type' must be a string"); + } + String type = parser.getText(); + parser.nextToken(); + return deserializeSelected(parser, context, type); + } + return deserializeBuffered(parser, context); + } + + private UnionTypeExample deserializeBuffered(JsonParser parser, DeserializationContext context) + throws IOException { + try (TokenBuffer buffer = context.bufferForInputBuffering(parser)) { + buffer.writeStartObject(); + JsonToken token = parser.currentToken(); + while (token == JsonToken.FIELD_NAME) { + String fieldName = parser.currentName(); + JsonToken valueToken = parser.nextToken(); + if (isTypeField(fieldName, context)) { + if (valueToken != JsonToken.VALUE_STRING) { + return context.reportInputMismatch( + UnionTypeExample.class, "Union discriminator 'type' must be a string"); + } + String type = parser.getText(); + parser.nextToken(); + try (JsonParser bufferedParser = buffer.asParser(parser)) { + JsonParser combinedParser = + JsonParserSequence.createFlattened(true, bufferedParser, parser); + combinedParser.nextToken(); + return deserializeSelected(combinedParser, context, type); + } + } + buffer.writeFieldName(fieldName); + buffer.copyCurrentStructure(parser); + token = parser.nextToken(); + } + if (token != JsonToken.END_OBJECT) { + return context.reportInputMismatch( + UnionTypeExample.class, "Expected the end of a JSON object while deserializing a union"); + } + } + return context.reportInputMismatch(UnionTypeExample.class, "Union discriminator 'type' is required"); + } + + private static boolean isTypeField(String fieldName, DeserializationContext context) { + return "type".equals(fieldName) + || (context.isEnabled(MapperFeature.ACCEPT_CASE_INSENSITIVE_PROPERTIES) + && "type".equalsIgnoreCase(fieldName)); + } + + private UnionTypeExample deserializeSelected(JsonParser parser, DeserializationContext context, String type) + throws IOException { + int variantIndex = + switch (type) { + case "stringExample" -> 0; + case "thisFieldIsAnInteger" -> 1; + case "alsoAnInteger" -> 2; + case "if" -> 3; + case "new" -> 4; + case "interface" -> 5; + case "completed" -> 6; + case "unknown" -> 7; + case "optional" -> 8; + case "list" -> 9; + case "set" -> 10; + case "map" -> 11; + case "optionalAlias" -> 12; + case "listAlias" -> 13; + case "setAlias" -> 14; + case "mapAlias" -> 15; + case "booleanField" -> 16; + case "safeInt" -> 17; + case "unsafeDouble" -> 18; + default -> -1; + }; + if (variantIndex < 0) { + return deserializeUnknown(parser, context, type); + } + JsonDeserializer deserializer = deserializers[variantIndex]; + if (deserializer == null) { + deserializer = resolveDeserializer(context, variantIndex); + } + return new UnionTypeExample((Base) deserializer.deserialize(parser, context)); + } + + private synchronized JsonDeserializer resolveDeserializer(DeserializationContext context, int variantIndex) + throws JsonMappingException { + JsonDeserializer deserializer = deserializers[variantIndex]; + if (deserializer == null) { + deserializer = context.findRootValueDeserializer(context.constructType(VARIANT_TYPES[variantIndex])); + JsonDeserializer[] updated = deserializers.clone(); + updated[variantIndex] = deserializer; + deserializers = updated; + } + return deserializer; + } + + private static UnionTypeExample deserializeUnknown( + JsonParser parser, DeserializationContext context, String type) throws IOException { + Map values = new HashMap<>(); + if (parser.currentToken() == JsonToken.START_OBJECT) { + parser.nextToken(); + } + while (parser.currentToken() == JsonToken.FIELD_NAME) { + String fieldName = parser.currentName(); + parser.nextToken(); + values.put(fieldName, context.readValue(parser, Object.class)); + parser.nextToken(); + } + if (parser.currentToken() != JsonToken.END_OBJECT) { + return context.reportInputMismatch( + UnionTypeExample.class, "Expected the end of a JSON object while deserializing a union"); + } + return new UnionTypeExample(new UnknownWrapper(type, values)); + } + } } diff --git a/conjure-java-core/src/integrationInput/java/allexamples/com/palantir/product/UnionWithUnknownString.java b/conjure-java-core/src/integrationInput/java/allexamples/com/palantir/product/UnionWithUnknownString.java index 1f7d8cee7..e7a0e5f3b 100644 --- a/conjure-java-core/src/integrationInput/java/allexamples/com/palantir/product/UnionWithUnknownString.java +++ b/conjure-java-core/src/integrationInput/java/allexamples/com/palantir/product/UnionWithUnknownString.java @@ -6,14 +6,23 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.annotation.JsonSetter; -import com.fasterxml.jackson.annotation.JsonSubTypes; -import com.fasterxml.jackson.annotation.JsonTypeInfo; import com.fasterxml.jackson.annotation.JsonTypeName; import com.fasterxml.jackson.annotation.JsonValue; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.JsonToken; +import com.fasterxml.jackson.core.util.JsonParserSequence; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonDeserializer; +import com.fasterxml.jackson.databind.JsonMappingException; +import com.fasterxml.jackson.databind.MapperFeature; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.deser.ResolvableDeserializer; +import com.fasterxml.jackson.databind.util.TokenBuffer; import com.palantir.logsafe.Preconditions; import com.palantir.logsafe.Safe; import com.palantir.logsafe.SafeArg; import com.palantir.logsafe.exceptions.SafeIllegalArgumentException; +import java.io.IOException; import java.util.Collections; import java.util.HashMap; import java.util.Map; @@ -24,6 +33,7 @@ import javax.annotation.processing.Generated; @Generated("com.palantir.conjure.java.types.UnionGenerator") +@JsonDeserialize(using = UnionWithUnknownString.Deserializer.class) public final class UnionWithUnknownString { private final Base value; @@ -155,19 +165,12 @@ public interface Completed_StageVisitorBuilder { Visitor build(); } - @JsonTypeInfo( - use = JsonTypeInfo.Id.NAME, - include = JsonTypeInfo.As.EXISTING_PROPERTY, - property = "type", - visible = true, - defaultImpl = UnknownWrapper.class) - @JsonSubTypes(@JsonSubTypes.Type(Unknown_Wrapper.class)) - @JsonIgnoreProperties(ignoreUnknown = true) private interface Base { T accept(Visitor visitor); } @JsonTypeName("unknown") + @JsonIgnoreProperties(ignoreUnknown = true) private static final class Unknown_Wrapper implements Base { private final String value; @@ -271,4 +274,129 @@ public String toString() { return "UnknownWrapper{type: " + type + ", value: " + value + '}'; } } + + static final class Deserializer extends JsonDeserializer implements ResolvableDeserializer { + private static final Class[] VARIANT_TYPES = new Class[] {Unknown_Wrapper.class}; + + private volatile JsonDeserializer[] deserializers; + + @Override + public void resolve(DeserializationContext context) throws JsonMappingException { + deserializers = new JsonDeserializer[VARIANT_TYPES.length]; + } + + @Override + public boolean isCachable() { + return true; + } + + @Override + public UnionWithUnknownString deserialize(JsonParser parser, DeserializationContext context) + throws IOException { + if (!parser.isExpectedStartObjectToken()) { + return context.reportInputMismatch( + UnionWithUnknownString.class, "Expected a JSON object for union deserialization"); + } + JsonToken firstToken = parser.nextToken(); + if (firstToken == JsonToken.FIELD_NAME && isTypeField(parser.currentName(), context)) { + if (parser.nextToken() != JsonToken.VALUE_STRING) { + return context.reportInputMismatch( + UnionWithUnknownString.class, "Union discriminator 'type' must be a string"); + } + String type = parser.getText(); + parser.nextToken(); + return deserializeSelected(parser, context, type); + } + return deserializeBuffered(parser, context); + } + + private UnionWithUnknownString deserializeBuffered(JsonParser parser, DeserializationContext context) + throws IOException { + try (TokenBuffer buffer = context.bufferForInputBuffering(parser)) { + buffer.writeStartObject(); + JsonToken token = parser.currentToken(); + while (token == JsonToken.FIELD_NAME) { + String fieldName = parser.currentName(); + JsonToken valueToken = parser.nextToken(); + if (isTypeField(fieldName, context)) { + if (valueToken != JsonToken.VALUE_STRING) { + return context.reportInputMismatch( + UnionWithUnknownString.class, "Union discriminator 'type' must be a string"); + } + String type = parser.getText(); + parser.nextToken(); + try (JsonParser bufferedParser = buffer.asParser(parser)) { + JsonParser combinedParser = + JsonParserSequence.createFlattened(true, bufferedParser, parser); + combinedParser.nextToken(); + return deserializeSelected(combinedParser, context, type); + } + } + buffer.writeFieldName(fieldName); + buffer.copyCurrentStructure(parser); + token = parser.nextToken(); + } + if (token != JsonToken.END_OBJECT) { + return context.reportInputMismatch( + UnionWithUnknownString.class, + "Expected the end of a JSON object while deserializing a union"); + } + } + return context.reportInputMismatch(UnionWithUnknownString.class, "Union discriminator 'type' is required"); + } + + private static boolean isTypeField(String fieldName, DeserializationContext context) { + return "type".equals(fieldName) + || (context.isEnabled(MapperFeature.ACCEPT_CASE_INSENSITIVE_PROPERTIES) + && "type".equalsIgnoreCase(fieldName)); + } + + private UnionWithUnknownString deserializeSelected( + JsonParser parser, DeserializationContext context, String type) throws IOException { + int variantIndex = + switch (type) { + case "unknown" -> 0; + default -> -1; + }; + if (variantIndex < 0) { + return deserializeUnknown(parser, context, type); + } + JsonDeserializer deserializer = deserializers[variantIndex]; + if (deserializer == null) { + deserializer = resolveDeserializer(context, variantIndex); + } + return new UnionWithUnknownString((Base) deserializer.deserialize(parser, context)); + } + + private synchronized JsonDeserializer resolveDeserializer(DeserializationContext context, int variantIndex) + throws JsonMappingException { + JsonDeserializer deserializer = deserializers[variantIndex]; + if (deserializer == null) { + deserializer = context.findRootValueDeserializer(context.constructType(VARIANT_TYPES[variantIndex])); + JsonDeserializer[] updated = deserializers.clone(); + updated[variantIndex] = deserializer; + deserializers = updated; + } + return deserializer; + } + + private static UnionWithUnknownString deserializeUnknown( + JsonParser parser, DeserializationContext context, String type) throws IOException { + Map values = new HashMap<>(); + if (parser.currentToken() == JsonToken.START_OBJECT) { + parser.nextToken(); + } + while (parser.currentToken() == JsonToken.FIELD_NAME) { + String fieldName = parser.currentName(); + parser.nextToken(); + values.put(fieldName, context.readValue(parser, Object.class)); + parser.nextToken(); + } + if (parser.currentToken() != JsonToken.END_OBJECT) { + return context.reportInputMismatch( + UnionWithUnknownString.class, "Expected the end of a JSON object while deserializing a union"); + } + return new UnionWithUnknownString(new UnknownWrapper(type, values)); + } + } } diff --git a/conjure-java-core/src/integrationInput/java/bothstrictstagedbuilder/com/palantir/product/StrictFourFields.java b/conjure-java-core/src/integrationInput/java/bothstrictstagedbuilder/com/palantir/product/StrictFourFields.java index d11fe9abe..6cd13a3f3 100644 --- a/conjure-java-core/src/integrationInput/java/bothstrictstagedbuilder/com/palantir/product/StrictFourFields.java +++ b/conjure-java-core/src/integrationInput/java/bothstrictstagedbuilder/com/palantir/product/StrictFourFields.java @@ -206,6 +206,7 @@ static final class DefaultBuilder implements Builder { private Optional optionalItem = Optional.empty(); + @JsonSetter(value = "mappedRids", nulls = Nulls.SKIP) private Map mappedRids = new LinkedHashMap<>(); private DefaultBuilder() {} @@ -252,7 +253,6 @@ public Builder optionalItem(@Nonnull String optionalItem) { } @Override - @JsonSetter(value = "mappedRids", nulls = Nulls.SKIP) public Builder mappedRids(@Nonnull Map mappedRids) { checkNotBuilt(); this.mappedRids = new LinkedHashMap<>(Preconditions.checkNotNull(mappedRids, "mappedRids cannot be null")); diff --git a/conjure-java-core/src/integrationInput/java/bothstrictstagedbuilder/com/palantir/product/StrictMultipleDeprecatedAndUnsafeFields.java b/conjure-java-core/src/integrationInput/java/bothstrictstagedbuilder/com/palantir/product/StrictMultipleDeprecatedAndUnsafeFields.java index 1fc34eb77..8318bf766 100644 --- a/conjure-java-core/src/integrationInput/java/bothstrictstagedbuilder/com/palantir/product/StrictMultipleDeprecatedAndUnsafeFields.java +++ b/conjure-java-core/src/integrationInput/java/bothstrictstagedbuilder/com/palantir/product/StrictMultipleDeprecatedAndUnsafeFields.java @@ -287,6 +287,7 @@ static final class DefaultBuilder implements Builder { private Optional<@Unsafe String> optionalItem = Optional.empty(); + @JsonSetter(value = "mappedRids", nulls = Nulls.SKIP) private Map mappedRids = new LinkedHashMap<>(); private StrictFourFields strictFourFieldsObject; @@ -365,7 +366,6 @@ public Builder optionalItem(@Nonnull @Unsafe String optionalItem) { } @Override - @JsonSetter(value = "mappedRids", nulls = Nulls.SKIP) public Builder mappedRids(@Nonnull Map mappedRids) { checkNotBuilt(); this.mappedRids = new LinkedHashMap<>(Preconditions.checkNotNull(mappedRids, "mappedRids cannot be null")); diff --git a/conjure-java-core/src/integrationInput/java/defensivenonnullcollections/com/palantir/product/ExampleDefensiveCollectionListsUnion.java b/conjure-java-core/src/integrationInput/java/defensivenonnullcollections/com/palantir/product/ExampleDefensiveCollectionListsUnion.java index aa5c37790..ee5d4bbe2 100644 --- a/conjure-java-core/src/integrationInput/java/defensivenonnullcollections/com/palantir/product/ExampleDefensiveCollectionListsUnion.java +++ b/conjure-java-core/src/integrationInput/java/defensivenonnullcollections/com/palantir/product/ExampleDefensiveCollectionListsUnion.java @@ -6,16 +6,25 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.annotation.JsonSetter; -import com.fasterxml.jackson.annotation.JsonSubTypes; -import com.fasterxml.jackson.annotation.JsonTypeInfo; import com.fasterxml.jackson.annotation.JsonTypeName; import com.fasterxml.jackson.annotation.JsonValue; import com.fasterxml.jackson.annotation.Nulls; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.JsonToken; +import com.fasterxml.jackson.core.util.JsonParserSequence; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonDeserializer; +import com.fasterxml.jackson.databind.JsonMappingException; +import com.fasterxml.jackson.databind.MapperFeature; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.deser.ResolvableDeserializer; +import com.fasterxml.jackson.databind.util.TokenBuffer; import com.palantir.conjure.java.lib.internal.ConjureCollections; import com.palantir.logsafe.Preconditions; import com.palantir.logsafe.Safe; import com.palantir.logsafe.SafeArg; import com.palantir.logsafe.exceptions.SafeIllegalArgumentException; +import java.io.IOException; import java.util.Collections; import java.util.HashMap; import java.util.List; @@ -27,6 +36,7 @@ import javax.annotation.processing.Generated; @Generated("com.palantir.conjure.java.types.UnionGenerator") +@JsonDeserialize(using = ExampleDefensiveCollectionListsUnion.Deserializer.class) public final class ExampleDefensiveCollectionListsUnion { private final Base value; @@ -214,23 +224,12 @@ public interface Completed_StageVisitorBuilder { Visitor build(); } - @JsonTypeInfo( - use = JsonTypeInfo.Id.NAME, - include = JsonTypeInfo.As.EXISTING_PROPERTY, - property = "type", - visible = true, - defaultImpl = UnknownWrapper.class) - @JsonSubTypes({ - @JsonSubTypes.Type(ListWrapper.class), - @JsonSubTypes.Type(PrimitiveListWrapper.class), - @JsonSubTypes.Type(ListOptionalWrapper.class) - }) - @JsonIgnoreProperties(ignoreUnknown = true) private interface Base { T accept(Visitor visitor); } @JsonTypeName("list") + @JsonIgnoreProperties(ignoreUnknown = true) private static final class ListWrapper implements Base { private final List value; @@ -276,6 +275,7 @@ public String toString() { } @JsonTypeName("primitiveList") + @JsonIgnoreProperties(ignoreUnknown = true) private static final class PrimitiveListWrapper implements Base { private final List value; @@ -322,6 +322,7 @@ public String toString() { } @JsonTypeName("listOptional") + @JsonIgnoreProperties(ignoreUnknown = true) private static final class ListOptionalWrapper implements Base { private final List> value; @@ -426,4 +427,136 @@ public String toString() { return "UnknownWrapper{type: " + type + ", value: " + value + '}'; } } + + static final class Deserializer extends JsonDeserializer + implements ResolvableDeserializer { + private static final Class[] VARIANT_TYPES = + new Class[] {ListWrapper.class, PrimitiveListWrapper.class, ListOptionalWrapper.class}; + + private volatile JsonDeserializer[] deserializers; + + @Override + public void resolve(DeserializationContext context) throws JsonMappingException { + deserializers = new JsonDeserializer[VARIANT_TYPES.length]; + } + + @Override + public boolean isCachable() { + return true; + } + + @Override + public ExampleDefensiveCollectionListsUnion deserialize(JsonParser parser, DeserializationContext context) + throws IOException { + if (!parser.isExpectedStartObjectToken()) { + return context.reportInputMismatch( + ExampleDefensiveCollectionListsUnion.class, "Expected a JSON object for union deserialization"); + } + JsonToken firstToken = parser.nextToken(); + if (firstToken == JsonToken.FIELD_NAME && isTypeField(parser.currentName(), context)) { + if (parser.nextToken() != JsonToken.VALUE_STRING) { + return context.reportInputMismatch( + ExampleDefensiveCollectionListsUnion.class, "Union discriminator 'type' must be a string"); + } + String type = parser.getText(); + parser.nextToken(); + return deserializeSelected(parser, context, type); + } + return deserializeBuffered(parser, context); + } + + private ExampleDefensiveCollectionListsUnion deserializeBuffered( + JsonParser parser, DeserializationContext context) throws IOException { + try (TokenBuffer buffer = context.bufferForInputBuffering(parser)) { + buffer.writeStartObject(); + JsonToken token = parser.currentToken(); + while (token == JsonToken.FIELD_NAME) { + String fieldName = parser.currentName(); + JsonToken valueToken = parser.nextToken(); + if (isTypeField(fieldName, context)) { + if (valueToken != JsonToken.VALUE_STRING) { + return context.reportInputMismatch( + ExampleDefensiveCollectionListsUnion.class, + "Union discriminator 'type' must be a string"); + } + String type = parser.getText(); + parser.nextToken(); + try (JsonParser bufferedParser = buffer.asParser(parser)) { + JsonParser combinedParser = + JsonParserSequence.createFlattened(true, bufferedParser, parser); + combinedParser.nextToken(); + return deserializeSelected(combinedParser, context, type); + } + } + buffer.writeFieldName(fieldName); + buffer.copyCurrentStructure(parser); + token = parser.nextToken(); + } + if (token != JsonToken.END_OBJECT) { + return context.reportInputMismatch( + ExampleDefensiveCollectionListsUnion.class, + "Expected the end of a JSON object while deserializing a union"); + } + } + return context.reportInputMismatch( + ExampleDefensiveCollectionListsUnion.class, "Union discriminator 'type' is required"); + } + + private static boolean isTypeField(String fieldName, DeserializationContext context) { + return "type".equals(fieldName) + || (context.isEnabled(MapperFeature.ACCEPT_CASE_INSENSITIVE_PROPERTIES) + && "type".equalsIgnoreCase(fieldName)); + } + + private ExampleDefensiveCollectionListsUnion deserializeSelected( + JsonParser parser, DeserializationContext context, String type) throws IOException { + int variantIndex = + switch (type) { + case "list" -> 0; + case "primitiveList" -> 1; + case "listOptional" -> 2; + default -> -1; + }; + if (variantIndex < 0) { + return deserializeUnknown(parser, context, type); + } + JsonDeserializer deserializer = deserializers[variantIndex]; + if (deserializer == null) { + deserializer = resolveDeserializer(context, variantIndex); + } + return new ExampleDefensiveCollectionListsUnion((Base) deserializer.deserialize(parser, context)); + } + + private synchronized JsonDeserializer resolveDeserializer(DeserializationContext context, int variantIndex) + throws JsonMappingException { + JsonDeserializer deserializer = deserializers[variantIndex]; + if (deserializer == null) { + deserializer = context.findRootValueDeserializer(context.constructType(VARIANT_TYPES[variantIndex])); + JsonDeserializer[] updated = deserializers.clone(); + updated[variantIndex] = deserializer; + deserializers = updated; + } + return deserializer; + } + + private static ExampleDefensiveCollectionListsUnion deserializeUnknown( + JsonParser parser, DeserializationContext context, String type) throws IOException { + Map values = new HashMap<>(); + if (parser.currentToken() == JsonToken.START_OBJECT) { + parser.nextToken(); + } + while (parser.currentToken() == JsonToken.FIELD_NAME) { + String fieldName = parser.currentName(); + parser.nextToken(); + values.put(fieldName, context.readValue(parser, Object.class)); + parser.nextToken(); + } + if (parser.currentToken() != JsonToken.END_OBJECT) { + return context.reportInputMismatch( + ExampleDefensiveCollectionListsUnion.class, + "Expected the end of a JSON object while deserializing a union"); + } + return new ExampleDefensiveCollectionListsUnion(new UnknownWrapper(type, values)); + } + } } diff --git a/conjure-java-core/src/integrationInput/java/defensivenonnullcollections/com/palantir/product/ExampleDefensiveCollectionMapsUnion.java b/conjure-java-core/src/integrationInput/java/defensivenonnullcollections/com/palantir/product/ExampleDefensiveCollectionMapsUnion.java index f13c83cfd..c0d6c8848 100644 --- a/conjure-java-core/src/integrationInput/java/defensivenonnullcollections/com/palantir/product/ExampleDefensiveCollectionMapsUnion.java +++ b/conjure-java-core/src/integrationInput/java/defensivenonnullcollections/com/palantir/product/ExampleDefensiveCollectionMapsUnion.java @@ -6,15 +6,24 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.annotation.JsonSetter; -import com.fasterxml.jackson.annotation.JsonSubTypes; -import com.fasterxml.jackson.annotation.JsonTypeInfo; import com.fasterxml.jackson.annotation.JsonTypeName; import com.fasterxml.jackson.annotation.JsonValue; import com.fasterxml.jackson.annotation.Nulls; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.JsonToken; +import com.fasterxml.jackson.core.util.JsonParserSequence; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonDeserializer; +import com.fasterxml.jackson.databind.JsonMappingException; +import com.fasterxml.jackson.databind.MapperFeature; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.deser.ResolvableDeserializer; +import com.fasterxml.jackson.databind.util.TokenBuffer; import com.palantir.logsafe.Preconditions; import com.palantir.logsafe.Safe; import com.palantir.logsafe.SafeArg; import com.palantir.logsafe.exceptions.SafeIllegalArgumentException; +import java.io.IOException; import java.util.Collections; import java.util.HashMap; import java.util.LinkedHashMap; @@ -26,6 +35,7 @@ import javax.annotation.processing.Generated; @Generated("com.palantir.conjure.java.types.UnionGenerator") +@JsonDeserialize(using = ExampleDefensiveCollectionMapsUnion.Deserializer.class) public final class ExampleDefensiveCollectionMapsUnion { private final Base value; @@ -184,19 +194,12 @@ public interface Completed_StageVisitorBuilder { Visitor build(); } - @JsonTypeInfo( - use = JsonTypeInfo.Id.NAME, - include = JsonTypeInfo.As.EXISTING_PROPERTY, - property = "type", - visible = true, - defaultImpl = UnknownWrapper.class) - @JsonSubTypes({@JsonSubTypes.Type(MapWrapper.class), @JsonSubTypes.Type(MapOptionalWrapper.class)}) - @JsonIgnoreProperties(ignoreUnknown = true) private interface Base { T accept(Visitor visitor); } @JsonTypeName("map") + @JsonIgnoreProperties(ignoreUnknown = true) private static final class MapWrapper implements Base { private final Map value; @@ -244,6 +247,7 @@ public String toString() { } @JsonTypeName("mapOptional") + @JsonIgnoreProperties(ignoreUnknown = true) private static final class MapOptionalWrapper implements Base { private final Map> value; @@ -349,4 +353,134 @@ public String toString() { return "UnknownWrapper{type: " + type + ", value: " + value + '}'; } } + + static final class Deserializer extends JsonDeserializer + implements ResolvableDeserializer { + private static final Class[] VARIANT_TYPES = new Class[] {MapWrapper.class, MapOptionalWrapper.class}; + + private volatile JsonDeserializer[] deserializers; + + @Override + public void resolve(DeserializationContext context) throws JsonMappingException { + deserializers = new JsonDeserializer[VARIANT_TYPES.length]; + } + + @Override + public boolean isCachable() { + return true; + } + + @Override + public ExampleDefensiveCollectionMapsUnion deserialize(JsonParser parser, DeserializationContext context) + throws IOException { + if (!parser.isExpectedStartObjectToken()) { + return context.reportInputMismatch( + ExampleDefensiveCollectionMapsUnion.class, "Expected a JSON object for union deserialization"); + } + JsonToken firstToken = parser.nextToken(); + if (firstToken == JsonToken.FIELD_NAME && isTypeField(parser.currentName(), context)) { + if (parser.nextToken() != JsonToken.VALUE_STRING) { + return context.reportInputMismatch( + ExampleDefensiveCollectionMapsUnion.class, "Union discriminator 'type' must be a string"); + } + String type = parser.getText(); + parser.nextToken(); + return deserializeSelected(parser, context, type); + } + return deserializeBuffered(parser, context); + } + + private ExampleDefensiveCollectionMapsUnion deserializeBuffered( + JsonParser parser, DeserializationContext context) throws IOException { + try (TokenBuffer buffer = context.bufferForInputBuffering(parser)) { + buffer.writeStartObject(); + JsonToken token = parser.currentToken(); + while (token == JsonToken.FIELD_NAME) { + String fieldName = parser.currentName(); + JsonToken valueToken = parser.nextToken(); + if (isTypeField(fieldName, context)) { + if (valueToken != JsonToken.VALUE_STRING) { + return context.reportInputMismatch( + ExampleDefensiveCollectionMapsUnion.class, + "Union discriminator 'type' must be a string"); + } + String type = parser.getText(); + parser.nextToken(); + try (JsonParser bufferedParser = buffer.asParser(parser)) { + JsonParser combinedParser = + JsonParserSequence.createFlattened(true, bufferedParser, parser); + combinedParser.nextToken(); + return deserializeSelected(combinedParser, context, type); + } + } + buffer.writeFieldName(fieldName); + buffer.copyCurrentStructure(parser); + token = parser.nextToken(); + } + if (token != JsonToken.END_OBJECT) { + return context.reportInputMismatch( + ExampleDefensiveCollectionMapsUnion.class, + "Expected the end of a JSON object while deserializing a union"); + } + } + return context.reportInputMismatch( + ExampleDefensiveCollectionMapsUnion.class, "Union discriminator 'type' is required"); + } + + private static boolean isTypeField(String fieldName, DeserializationContext context) { + return "type".equals(fieldName) + || (context.isEnabled(MapperFeature.ACCEPT_CASE_INSENSITIVE_PROPERTIES) + && "type".equalsIgnoreCase(fieldName)); + } + + private ExampleDefensiveCollectionMapsUnion deserializeSelected( + JsonParser parser, DeserializationContext context, String type) throws IOException { + int variantIndex = + switch (type) { + case "map" -> 0; + case "mapOptional" -> 1; + default -> -1; + }; + if (variantIndex < 0) { + return deserializeUnknown(parser, context, type); + } + JsonDeserializer deserializer = deserializers[variantIndex]; + if (deserializer == null) { + deserializer = resolveDeserializer(context, variantIndex); + } + return new ExampleDefensiveCollectionMapsUnion((Base) deserializer.deserialize(parser, context)); + } + + private synchronized JsonDeserializer resolveDeserializer(DeserializationContext context, int variantIndex) + throws JsonMappingException { + JsonDeserializer deserializer = deserializers[variantIndex]; + if (deserializer == null) { + deserializer = context.findRootValueDeserializer(context.constructType(VARIANT_TYPES[variantIndex])); + JsonDeserializer[] updated = deserializers.clone(); + updated[variantIndex] = deserializer; + deserializers = updated; + } + return deserializer; + } + + private static ExampleDefensiveCollectionMapsUnion deserializeUnknown( + JsonParser parser, DeserializationContext context, String type) throws IOException { + Map values = new HashMap<>(); + if (parser.currentToken() == JsonToken.START_OBJECT) { + parser.nextToken(); + } + while (parser.currentToken() == JsonToken.FIELD_NAME) { + String fieldName = parser.currentName(); + parser.nextToken(); + values.put(fieldName, context.readValue(parser, Object.class)); + parser.nextToken(); + } + if (parser.currentToken() != JsonToken.END_OBJECT) { + return context.reportInputMismatch( + ExampleDefensiveCollectionMapsUnion.class, + "Expected the end of a JSON object while deserializing a union"); + } + return new ExampleDefensiveCollectionMapsUnion(new UnknownWrapper(type, values)); + } + } } diff --git a/conjure-java-core/src/integrationInput/java/defensivenonnullcollections/com/palantir/product/ExampleDefensiveCollectionSetsUnion.java b/conjure-java-core/src/integrationInput/java/defensivenonnullcollections/com/palantir/product/ExampleDefensiveCollectionSetsUnion.java index 46558cc99..ebf32d516 100644 --- a/conjure-java-core/src/integrationInput/java/defensivenonnullcollections/com/palantir/product/ExampleDefensiveCollectionSetsUnion.java +++ b/conjure-java-core/src/integrationInput/java/defensivenonnullcollections/com/palantir/product/ExampleDefensiveCollectionSetsUnion.java @@ -6,17 +6,25 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.annotation.JsonSetter; -import com.fasterxml.jackson.annotation.JsonSubTypes; -import com.fasterxml.jackson.annotation.JsonTypeInfo; import com.fasterxml.jackson.annotation.JsonTypeName; import com.fasterxml.jackson.annotation.JsonValue; import com.fasterxml.jackson.annotation.Nulls; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.JsonToken; +import com.fasterxml.jackson.core.util.JsonParserSequence; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonDeserializer; +import com.fasterxml.jackson.databind.JsonMappingException; +import com.fasterxml.jackson.databind.MapperFeature; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.deser.ResolvableDeserializer; +import com.fasterxml.jackson.databind.util.TokenBuffer; import com.palantir.conjure.java.lib.internal.ConjureCollections; import com.palantir.logsafe.Preconditions; import com.palantir.logsafe.Safe; import com.palantir.logsafe.SafeArg; import com.palantir.logsafe.exceptions.SafeIllegalArgumentException; +import java.io.IOException; import java.util.Collections; import java.util.HashMap; import java.util.LinkedHashSet; @@ -28,6 +36,7 @@ import javax.annotation.processing.Generated; @Generated("com.palantir.conjure.java.types.UnionGenerator") +@JsonDeserialize(using = ExampleDefensiveCollectionSetsUnion.Deserializer.class) public final class ExampleDefensiveCollectionSetsUnion { private final Base value; @@ -153,19 +162,12 @@ public interface Completed_StageVisitorBuilder { Visitor build(); } - @JsonTypeInfo( - use = JsonTypeInfo.Id.NAME, - include = JsonTypeInfo.As.EXISTING_PROPERTY, - property = "type", - visible = true, - defaultImpl = UnknownWrapper.class) - @JsonSubTypes(@JsonSubTypes.Type(SetWrapper.class)) - @JsonIgnoreProperties(ignoreUnknown = true) private interface Base { T accept(Visitor visitor); } @JsonTypeName("set") + @JsonIgnoreProperties(ignoreUnknown = true) private static final class SetWrapper implements Base { private final Set value; @@ -271,4 +273,133 @@ public String toString() { return "UnknownWrapper{type: " + type + ", value: " + value + '}'; } } + + static final class Deserializer extends JsonDeserializer + implements ResolvableDeserializer { + private static final Class[] VARIANT_TYPES = new Class[] {SetWrapper.class}; + + private volatile JsonDeserializer[] deserializers; + + @Override + public void resolve(DeserializationContext context) throws JsonMappingException { + deserializers = new JsonDeserializer[VARIANT_TYPES.length]; + } + + @Override + public boolean isCachable() { + return true; + } + + @Override + public ExampleDefensiveCollectionSetsUnion deserialize(JsonParser parser, DeserializationContext context) + throws IOException { + if (!parser.isExpectedStartObjectToken()) { + return context.reportInputMismatch( + ExampleDefensiveCollectionSetsUnion.class, "Expected a JSON object for union deserialization"); + } + JsonToken firstToken = parser.nextToken(); + if (firstToken == JsonToken.FIELD_NAME && isTypeField(parser.currentName(), context)) { + if (parser.nextToken() != JsonToken.VALUE_STRING) { + return context.reportInputMismatch( + ExampleDefensiveCollectionSetsUnion.class, "Union discriminator 'type' must be a string"); + } + String type = parser.getText(); + parser.nextToken(); + return deserializeSelected(parser, context, type); + } + return deserializeBuffered(parser, context); + } + + private ExampleDefensiveCollectionSetsUnion deserializeBuffered( + JsonParser parser, DeserializationContext context) throws IOException { + try (TokenBuffer buffer = context.bufferForInputBuffering(parser)) { + buffer.writeStartObject(); + JsonToken token = parser.currentToken(); + while (token == JsonToken.FIELD_NAME) { + String fieldName = parser.currentName(); + JsonToken valueToken = parser.nextToken(); + if (isTypeField(fieldName, context)) { + if (valueToken != JsonToken.VALUE_STRING) { + return context.reportInputMismatch( + ExampleDefensiveCollectionSetsUnion.class, + "Union discriminator 'type' must be a string"); + } + String type = parser.getText(); + parser.nextToken(); + try (JsonParser bufferedParser = buffer.asParser(parser)) { + JsonParser combinedParser = + JsonParserSequence.createFlattened(true, bufferedParser, parser); + combinedParser.nextToken(); + return deserializeSelected(combinedParser, context, type); + } + } + buffer.writeFieldName(fieldName); + buffer.copyCurrentStructure(parser); + token = parser.nextToken(); + } + if (token != JsonToken.END_OBJECT) { + return context.reportInputMismatch( + ExampleDefensiveCollectionSetsUnion.class, + "Expected the end of a JSON object while deserializing a union"); + } + } + return context.reportInputMismatch( + ExampleDefensiveCollectionSetsUnion.class, "Union discriminator 'type' is required"); + } + + private static boolean isTypeField(String fieldName, DeserializationContext context) { + return "type".equals(fieldName) + || (context.isEnabled(MapperFeature.ACCEPT_CASE_INSENSITIVE_PROPERTIES) + && "type".equalsIgnoreCase(fieldName)); + } + + private ExampleDefensiveCollectionSetsUnion deserializeSelected( + JsonParser parser, DeserializationContext context, String type) throws IOException { + int variantIndex = + switch (type) { + case "set" -> 0; + default -> -1; + }; + if (variantIndex < 0) { + return deserializeUnknown(parser, context, type); + } + JsonDeserializer deserializer = deserializers[variantIndex]; + if (deserializer == null) { + deserializer = resolveDeserializer(context, variantIndex); + } + return new ExampleDefensiveCollectionSetsUnion((Base) deserializer.deserialize(parser, context)); + } + + private synchronized JsonDeserializer resolveDeserializer(DeserializationContext context, int variantIndex) + throws JsonMappingException { + JsonDeserializer deserializer = deserializers[variantIndex]; + if (deserializer == null) { + deserializer = context.findRootValueDeserializer(context.constructType(VARIANT_TYPES[variantIndex])); + JsonDeserializer[] updated = deserializers.clone(); + updated[variantIndex] = deserializer; + deserializers = updated; + } + return deserializer; + } + + private static ExampleDefensiveCollectionSetsUnion deserializeUnknown( + JsonParser parser, DeserializationContext context, String type) throws IOException { + Map values = new HashMap<>(); + if (parser.currentToken() == JsonToken.START_OBJECT) { + parser.nextToken(); + } + while (parser.currentToken() == JsonToken.FIELD_NAME) { + String fieldName = parser.currentName(); + parser.nextToken(); + values.put(fieldName, context.readValue(parser, Object.class)); + parser.nextToken(); + } + if (parser.currentToken() != JsonToken.END_OBJECT) { + return context.reportInputMismatch( + ExampleDefensiveCollectionSetsUnion.class, + "Expected the end of a JSON object while deserializing a union"); + } + return new ExampleDefensiveCollectionSetsUnion(new UnknownWrapper(type, values)); + } + } } diff --git a/conjure-java-core/src/integrationInput/java/defensivenullablecollections/com/palantir/product/ExampleDefensiveCollectionListsUnion.java b/conjure-java-core/src/integrationInput/java/defensivenullablecollections/com/palantir/product/ExampleDefensiveCollectionListsUnion.java index 59d2800bd..1d0c6475b 100644 --- a/conjure-java-core/src/integrationInput/java/defensivenullablecollections/com/palantir/product/ExampleDefensiveCollectionListsUnion.java +++ b/conjure-java-core/src/integrationInput/java/defensivenullablecollections/com/palantir/product/ExampleDefensiveCollectionListsUnion.java @@ -6,16 +6,25 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.annotation.JsonSetter; -import com.fasterxml.jackson.annotation.JsonSubTypes; -import com.fasterxml.jackson.annotation.JsonTypeInfo; import com.fasterxml.jackson.annotation.JsonTypeName; import com.fasterxml.jackson.annotation.JsonValue; import com.fasterxml.jackson.annotation.Nulls; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.JsonToken; +import com.fasterxml.jackson.core.util.JsonParserSequence; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonDeserializer; +import com.fasterxml.jackson.databind.JsonMappingException; +import com.fasterxml.jackson.databind.MapperFeature; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.deser.ResolvableDeserializer; +import com.fasterxml.jackson.databind.util.TokenBuffer; import com.palantir.conjure.java.lib.internal.ConjureCollections; import com.palantir.logsafe.Preconditions; import com.palantir.logsafe.Safe; import com.palantir.logsafe.SafeArg; import com.palantir.logsafe.exceptions.SafeIllegalArgumentException; +import java.io.IOException; import java.util.Collections; import java.util.HashMap; import java.util.List; @@ -27,6 +36,7 @@ import javax.annotation.processing.Generated; @Generated("com.palantir.conjure.java.types.UnionGenerator") +@JsonDeserialize(using = ExampleDefensiveCollectionListsUnion.Deserializer.class) public final class ExampleDefensiveCollectionListsUnion { private final Base value; @@ -214,23 +224,12 @@ public interface Completed_StageVisitorBuilder { Visitor build(); } - @JsonTypeInfo( - use = JsonTypeInfo.Id.NAME, - include = JsonTypeInfo.As.EXISTING_PROPERTY, - property = "type", - visible = true, - defaultImpl = UnknownWrapper.class) - @JsonSubTypes({ - @JsonSubTypes.Type(ListWrapper.class), - @JsonSubTypes.Type(PrimitiveListWrapper.class), - @JsonSubTypes.Type(ListOptionalWrapper.class) - }) - @JsonIgnoreProperties(ignoreUnknown = true) private interface Base { T accept(Visitor visitor); } @JsonTypeName("list") + @JsonIgnoreProperties(ignoreUnknown = true) private static final class ListWrapper implements Base { private final List value; @@ -276,6 +275,7 @@ public String toString() { } @JsonTypeName("primitiveList") + @JsonIgnoreProperties(ignoreUnknown = true) private static final class PrimitiveListWrapper implements Base { private final List value; @@ -322,6 +322,7 @@ public String toString() { } @JsonTypeName("listOptional") + @JsonIgnoreProperties(ignoreUnknown = true) private static final class ListOptionalWrapper implements Base { private final List> value; @@ -426,4 +427,136 @@ public String toString() { return "UnknownWrapper{type: " + type + ", value: " + value + '}'; } } + + static final class Deserializer extends JsonDeserializer + implements ResolvableDeserializer { + private static final Class[] VARIANT_TYPES = + new Class[] {ListWrapper.class, PrimitiveListWrapper.class, ListOptionalWrapper.class}; + + private volatile JsonDeserializer[] deserializers; + + @Override + public void resolve(DeserializationContext context) throws JsonMappingException { + deserializers = new JsonDeserializer[VARIANT_TYPES.length]; + } + + @Override + public boolean isCachable() { + return true; + } + + @Override + public ExampleDefensiveCollectionListsUnion deserialize(JsonParser parser, DeserializationContext context) + throws IOException { + if (!parser.isExpectedStartObjectToken()) { + return context.reportInputMismatch( + ExampleDefensiveCollectionListsUnion.class, "Expected a JSON object for union deserialization"); + } + JsonToken firstToken = parser.nextToken(); + if (firstToken == JsonToken.FIELD_NAME && isTypeField(parser.currentName(), context)) { + if (parser.nextToken() != JsonToken.VALUE_STRING) { + return context.reportInputMismatch( + ExampleDefensiveCollectionListsUnion.class, "Union discriminator 'type' must be a string"); + } + String type = parser.getText(); + parser.nextToken(); + return deserializeSelected(parser, context, type); + } + return deserializeBuffered(parser, context); + } + + private ExampleDefensiveCollectionListsUnion deserializeBuffered( + JsonParser parser, DeserializationContext context) throws IOException { + try (TokenBuffer buffer = context.bufferForInputBuffering(parser)) { + buffer.writeStartObject(); + JsonToken token = parser.currentToken(); + while (token == JsonToken.FIELD_NAME) { + String fieldName = parser.currentName(); + JsonToken valueToken = parser.nextToken(); + if (isTypeField(fieldName, context)) { + if (valueToken != JsonToken.VALUE_STRING) { + return context.reportInputMismatch( + ExampleDefensiveCollectionListsUnion.class, + "Union discriminator 'type' must be a string"); + } + String type = parser.getText(); + parser.nextToken(); + try (JsonParser bufferedParser = buffer.asParser(parser)) { + JsonParser combinedParser = + JsonParserSequence.createFlattened(true, bufferedParser, parser); + combinedParser.nextToken(); + return deserializeSelected(combinedParser, context, type); + } + } + buffer.writeFieldName(fieldName); + buffer.copyCurrentStructure(parser); + token = parser.nextToken(); + } + if (token != JsonToken.END_OBJECT) { + return context.reportInputMismatch( + ExampleDefensiveCollectionListsUnion.class, + "Expected the end of a JSON object while deserializing a union"); + } + } + return context.reportInputMismatch( + ExampleDefensiveCollectionListsUnion.class, "Union discriminator 'type' is required"); + } + + private static boolean isTypeField(String fieldName, DeserializationContext context) { + return "type".equals(fieldName) + || (context.isEnabled(MapperFeature.ACCEPT_CASE_INSENSITIVE_PROPERTIES) + && "type".equalsIgnoreCase(fieldName)); + } + + private ExampleDefensiveCollectionListsUnion deserializeSelected( + JsonParser parser, DeserializationContext context, String type) throws IOException { + int variantIndex = + switch (type) { + case "list" -> 0; + case "primitiveList" -> 1; + case "listOptional" -> 2; + default -> -1; + }; + if (variantIndex < 0) { + return deserializeUnknown(parser, context, type); + } + JsonDeserializer deserializer = deserializers[variantIndex]; + if (deserializer == null) { + deserializer = resolveDeserializer(context, variantIndex); + } + return new ExampleDefensiveCollectionListsUnion((Base) deserializer.deserialize(parser, context)); + } + + private synchronized JsonDeserializer resolveDeserializer(DeserializationContext context, int variantIndex) + throws JsonMappingException { + JsonDeserializer deserializer = deserializers[variantIndex]; + if (deserializer == null) { + deserializer = context.findRootValueDeserializer(context.constructType(VARIANT_TYPES[variantIndex])); + JsonDeserializer[] updated = deserializers.clone(); + updated[variantIndex] = deserializer; + deserializers = updated; + } + return deserializer; + } + + private static ExampleDefensiveCollectionListsUnion deserializeUnknown( + JsonParser parser, DeserializationContext context, String type) throws IOException { + Map values = new HashMap<>(); + if (parser.currentToken() == JsonToken.START_OBJECT) { + parser.nextToken(); + } + while (parser.currentToken() == JsonToken.FIELD_NAME) { + String fieldName = parser.currentName(); + parser.nextToken(); + values.put(fieldName, context.readValue(parser, Object.class)); + parser.nextToken(); + } + if (parser.currentToken() != JsonToken.END_OBJECT) { + return context.reportInputMismatch( + ExampleDefensiveCollectionListsUnion.class, + "Expected the end of a JSON object while deserializing a union"); + } + return new ExampleDefensiveCollectionListsUnion(new UnknownWrapper(type, values)); + } + } } diff --git a/conjure-java-core/src/integrationInput/java/defensivenullablecollections/com/palantir/product/ExampleDefensiveCollectionMapsUnion.java b/conjure-java-core/src/integrationInput/java/defensivenullablecollections/com/palantir/product/ExampleDefensiveCollectionMapsUnion.java index 89b8b126b..be925f0ee 100644 --- a/conjure-java-core/src/integrationInput/java/defensivenullablecollections/com/palantir/product/ExampleDefensiveCollectionMapsUnion.java +++ b/conjure-java-core/src/integrationInput/java/defensivenullablecollections/com/palantir/product/ExampleDefensiveCollectionMapsUnion.java @@ -6,15 +6,24 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.annotation.JsonSetter; -import com.fasterxml.jackson.annotation.JsonSubTypes; -import com.fasterxml.jackson.annotation.JsonTypeInfo; import com.fasterxml.jackson.annotation.JsonTypeName; import com.fasterxml.jackson.annotation.JsonValue; import com.fasterxml.jackson.annotation.Nulls; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.JsonToken; +import com.fasterxml.jackson.core.util.JsonParserSequence; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonDeserializer; +import com.fasterxml.jackson.databind.JsonMappingException; +import com.fasterxml.jackson.databind.MapperFeature; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.deser.ResolvableDeserializer; +import com.fasterxml.jackson.databind.util.TokenBuffer; import com.palantir.logsafe.Preconditions; import com.palantir.logsafe.Safe; import com.palantir.logsafe.SafeArg; import com.palantir.logsafe.exceptions.SafeIllegalArgumentException; +import java.io.IOException; import java.util.Collections; import java.util.HashMap; import java.util.LinkedHashMap; @@ -26,6 +35,7 @@ import javax.annotation.processing.Generated; @Generated("com.palantir.conjure.java.types.UnionGenerator") +@JsonDeserialize(using = ExampleDefensiveCollectionMapsUnion.Deserializer.class) public final class ExampleDefensiveCollectionMapsUnion { private final Base value; @@ -184,19 +194,12 @@ public interface Completed_StageVisitorBuilder { Visitor build(); } - @JsonTypeInfo( - use = JsonTypeInfo.Id.NAME, - include = JsonTypeInfo.As.EXISTING_PROPERTY, - property = "type", - visible = true, - defaultImpl = UnknownWrapper.class) - @JsonSubTypes({@JsonSubTypes.Type(MapWrapper.class), @JsonSubTypes.Type(MapOptionalWrapper.class)}) - @JsonIgnoreProperties(ignoreUnknown = true) private interface Base { T accept(Visitor visitor); } @JsonTypeName("map") + @JsonIgnoreProperties(ignoreUnknown = true) private static final class MapWrapper implements Base { private final Map value; @@ -242,6 +245,7 @@ public String toString() { } @JsonTypeName("mapOptional") + @JsonIgnoreProperties(ignoreUnknown = true) private static final class MapOptionalWrapper implements Base { private final Map> value; @@ -347,4 +351,134 @@ public String toString() { return "UnknownWrapper{type: " + type + ", value: " + value + '}'; } } + + static final class Deserializer extends JsonDeserializer + implements ResolvableDeserializer { + private static final Class[] VARIANT_TYPES = new Class[] {MapWrapper.class, MapOptionalWrapper.class}; + + private volatile JsonDeserializer[] deserializers; + + @Override + public void resolve(DeserializationContext context) throws JsonMappingException { + deserializers = new JsonDeserializer[VARIANT_TYPES.length]; + } + + @Override + public boolean isCachable() { + return true; + } + + @Override + public ExampleDefensiveCollectionMapsUnion deserialize(JsonParser parser, DeserializationContext context) + throws IOException { + if (!parser.isExpectedStartObjectToken()) { + return context.reportInputMismatch( + ExampleDefensiveCollectionMapsUnion.class, "Expected a JSON object for union deserialization"); + } + JsonToken firstToken = parser.nextToken(); + if (firstToken == JsonToken.FIELD_NAME && isTypeField(parser.currentName(), context)) { + if (parser.nextToken() != JsonToken.VALUE_STRING) { + return context.reportInputMismatch( + ExampleDefensiveCollectionMapsUnion.class, "Union discriminator 'type' must be a string"); + } + String type = parser.getText(); + parser.nextToken(); + return deserializeSelected(parser, context, type); + } + return deserializeBuffered(parser, context); + } + + private ExampleDefensiveCollectionMapsUnion deserializeBuffered( + JsonParser parser, DeserializationContext context) throws IOException { + try (TokenBuffer buffer = context.bufferForInputBuffering(parser)) { + buffer.writeStartObject(); + JsonToken token = parser.currentToken(); + while (token == JsonToken.FIELD_NAME) { + String fieldName = parser.currentName(); + JsonToken valueToken = parser.nextToken(); + if (isTypeField(fieldName, context)) { + if (valueToken != JsonToken.VALUE_STRING) { + return context.reportInputMismatch( + ExampleDefensiveCollectionMapsUnion.class, + "Union discriminator 'type' must be a string"); + } + String type = parser.getText(); + parser.nextToken(); + try (JsonParser bufferedParser = buffer.asParser(parser)) { + JsonParser combinedParser = + JsonParserSequence.createFlattened(true, bufferedParser, parser); + combinedParser.nextToken(); + return deserializeSelected(combinedParser, context, type); + } + } + buffer.writeFieldName(fieldName); + buffer.copyCurrentStructure(parser); + token = parser.nextToken(); + } + if (token != JsonToken.END_OBJECT) { + return context.reportInputMismatch( + ExampleDefensiveCollectionMapsUnion.class, + "Expected the end of a JSON object while deserializing a union"); + } + } + return context.reportInputMismatch( + ExampleDefensiveCollectionMapsUnion.class, "Union discriminator 'type' is required"); + } + + private static boolean isTypeField(String fieldName, DeserializationContext context) { + return "type".equals(fieldName) + || (context.isEnabled(MapperFeature.ACCEPT_CASE_INSENSITIVE_PROPERTIES) + && "type".equalsIgnoreCase(fieldName)); + } + + private ExampleDefensiveCollectionMapsUnion deserializeSelected( + JsonParser parser, DeserializationContext context, String type) throws IOException { + int variantIndex = + switch (type) { + case "map" -> 0; + case "mapOptional" -> 1; + default -> -1; + }; + if (variantIndex < 0) { + return deserializeUnknown(parser, context, type); + } + JsonDeserializer deserializer = deserializers[variantIndex]; + if (deserializer == null) { + deserializer = resolveDeserializer(context, variantIndex); + } + return new ExampleDefensiveCollectionMapsUnion((Base) deserializer.deserialize(parser, context)); + } + + private synchronized JsonDeserializer resolveDeserializer(DeserializationContext context, int variantIndex) + throws JsonMappingException { + JsonDeserializer deserializer = deserializers[variantIndex]; + if (deserializer == null) { + deserializer = context.findRootValueDeserializer(context.constructType(VARIANT_TYPES[variantIndex])); + JsonDeserializer[] updated = deserializers.clone(); + updated[variantIndex] = deserializer; + deserializers = updated; + } + return deserializer; + } + + private static ExampleDefensiveCollectionMapsUnion deserializeUnknown( + JsonParser parser, DeserializationContext context, String type) throws IOException { + Map values = new HashMap<>(); + if (parser.currentToken() == JsonToken.START_OBJECT) { + parser.nextToken(); + } + while (parser.currentToken() == JsonToken.FIELD_NAME) { + String fieldName = parser.currentName(); + parser.nextToken(); + values.put(fieldName, context.readValue(parser, Object.class)); + parser.nextToken(); + } + if (parser.currentToken() != JsonToken.END_OBJECT) { + return context.reportInputMismatch( + ExampleDefensiveCollectionMapsUnion.class, + "Expected the end of a JSON object while deserializing a union"); + } + return new ExampleDefensiveCollectionMapsUnion(new UnknownWrapper(type, values)); + } + } } diff --git a/conjure-java-core/src/integrationInput/java/defensivenullablecollections/com/palantir/product/ExampleDefensiveCollectionSetsUnion.java b/conjure-java-core/src/integrationInput/java/defensivenullablecollections/com/palantir/product/ExampleDefensiveCollectionSetsUnion.java index 1ca7a45d5..e5473d0a9 100644 --- a/conjure-java-core/src/integrationInput/java/defensivenullablecollections/com/palantir/product/ExampleDefensiveCollectionSetsUnion.java +++ b/conjure-java-core/src/integrationInput/java/defensivenullablecollections/com/palantir/product/ExampleDefensiveCollectionSetsUnion.java @@ -6,17 +6,25 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.annotation.JsonSetter; -import com.fasterxml.jackson.annotation.JsonSubTypes; -import com.fasterxml.jackson.annotation.JsonTypeInfo; import com.fasterxml.jackson.annotation.JsonTypeName; import com.fasterxml.jackson.annotation.JsonValue; import com.fasterxml.jackson.annotation.Nulls; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.JsonToken; +import com.fasterxml.jackson.core.util.JsonParserSequence; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonDeserializer; +import com.fasterxml.jackson.databind.JsonMappingException; +import com.fasterxml.jackson.databind.MapperFeature; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.deser.ResolvableDeserializer; +import com.fasterxml.jackson.databind.util.TokenBuffer; import com.palantir.conjure.java.lib.internal.ConjureCollections; import com.palantir.logsafe.Preconditions; import com.palantir.logsafe.Safe; import com.palantir.logsafe.SafeArg; import com.palantir.logsafe.exceptions.SafeIllegalArgumentException; +import java.io.IOException; import java.util.Collections; import java.util.HashMap; import java.util.LinkedHashSet; @@ -28,6 +36,7 @@ import javax.annotation.processing.Generated; @Generated("com.palantir.conjure.java.types.UnionGenerator") +@JsonDeserialize(using = ExampleDefensiveCollectionSetsUnion.Deserializer.class) public final class ExampleDefensiveCollectionSetsUnion { private final Base value; @@ -153,19 +162,12 @@ public interface Completed_StageVisitorBuilder { Visitor build(); } - @JsonTypeInfo( - use = JsonTypeInfo.Id.NAME, - include = JsonTypeInfo.As.EXISTING_PROPERTY, - property = "type", - visible = true, - defaultImpl = UnknownWrapper.class) - @JsonSubTypes(@JsonSubTypes.Type(SetWrapper.class)) - @JsonIgnoreProperties(ignoreUnknown = true) private interface Base { T accept(Visitor visitor); } @JsonTypeName("set") + @JsonIgnoreProperties(ignoreUnknown = true) private static final class SetWrapper implements Base { private final Set value; @@ -271,4 +273,133 @@ public String toString() { return "UnknownWrapper{type: " + type + ", value: " + value + '}'; } } + + static final class Deserializer extends JsonDeserializer + implements ResolvableDeserializer { + private static final Class[] VARIANT_TYPES = new Class[] {SetWrapper.class}; + + private volatile JsonDeserializer[] deserializers; + + @Override + public void resolve(DeserializationContext context) throws JsonMappingException { + deserializers = new JsonDeserializer[VARIANT_TYPES.length]; + } + + @Override + public boolean isCachable() { + return true; + } + + @Override + public ExampleDefensiveCollectionSetsUnion deserialize(JsonParser parser, DeserializationContext context) + throws IOException { + if (!parser.isExpectedStartObjectToken()) { + return context.reportInputMismatch( + ExampleDefensiveCollectionSetsUnion.class, "Expected a JSON object for union deserialization"); + } + JsonToken firstToken = parser.nextToken(); + if (firstToken == JsonToken.FIELD_NAME && isTypeField(parser.currentName(), context)) { + if (parser.nextToken() != JsonToken.VALUE_STRING) { + return context.reportInputMismatch( + ExampleDefensiveCollectionSetsUnion.class, "Union discriminator 'type' must be a string"); + } + String type = parser.getText(); + parser.nextToken(); + return deserializeSelected(parser, context, type); + } + return deserializeBuffered(parser, context); + } + + private ExampleDefensiveCollectionSetsUnion deserializeBuffered( + JsonParser parser, DeserializationContext context) throws IOException { + try (TokenBuffer buffer = context.bufferForInputBuffering(parser)) { + buffer.writeStartObject(); + JsonToken token = parser.currentToken(); + while (token == JsonToken.FIELD_NAME) { + String fieldName = parser.currentName(); + JsonToken valueToken = parser.nextToken(); + if (isTypeField(fieldName, context)) { + if (valueToken != JsonToken.VALUE_STRING) { + return context.reportInputMismatch( + ExampleDefensiveCollectionSetsUnion.class, + "Union discriminator 'type' must be a string"); + } + String type = parser.getText(); + parser.nextToken(); + try (JsonParser bufferedParser = buffer.asParser(parser)) { + JsonParser combinedParser = + JsonParserSequence.createFlattened(true, bufferedParser, parser); + combinedParser.nextToken(); + return deserializeSelected(combinedParser, context, type); + } + } + buffer.writeFieldName(fieldName); + buffer.copyCurrentStructure(parser); + token = parser.nextToken(); + } + if (token != JsonToken.END_OBJECT) { + return context.reportInputMismatch( + ExampleDefensiveCollectionSetsUnion.class, + "Expected the end of a JSON object while deserializing a union"); + } + } + return context.reportInputMismatch( + ExampleDefensiveCollectionSetsUnion.class, "Union discriminator 'type' is required"); + } + + private static boolean isTypeField(String fieldName, DeserializationContext context) { + return "type".equals(fieldName) + || (context.isEnabled(MapperFeature.ACCEPT_CASE_INSENSITIVE_PROPERTIES) + && "type".equalsIgnoreCase(fieldName)); + } + + private ExampleDefensiveCollectionSetsUnion deserializeSelected( + JsonParser parser, DeserializationContext context, String type) throws IOException { + int variantIndex = + switch (type) { + case "set" -> 0; + default -> -1; + }; + if (variantIndex < 0) { + return deserializeUnknown(parser, context, type); + } + JsonDeserializer deserializer = deserializers[variantIndex]; + if (deserializer == null) { + deserializer = resolveDeserializer(context, variantIndex); + } + return new ExampleDefensiveCollectionSetsUnion((Base) deserializer.deserialize(parser, context)); + } + + private synchronized JsonDeserializer resolveDeserializer(DeserializationContext context, int variantIndex) + throws JsonMappingException { + JsonDeserializer deserializer = deserializers[variantIndex]; + if (deserializer == null) { + deserializer = context.findRootValueDeserializer(context.constructType(VARIANT_TYPES[variantIndex])); + JsonDeserializer[] updated = deserializers.clone(); + updated[variantIndex] = deserializer; + deserializers = updated; + } + return deserializer; + } + + private static ExampleDefensiveCollectionSetsUnion deserializeUnknown( + JsonParser parser, DeserializationContext context, String type) throws IOException { + Map values = new HashMap<>(); + if (parser.currentToken() == JsonToken.START_OBJECT) { + parser.nextToken(); + } + while (parser.currentToken() == JsonToken.FIELD_NAME) { + String fieldName = parser.currentName(); + parser.nextToken(); + values.put(fieldName, context.readValue(parser, Object.class)); + parser.nextToken(); + } + if (parser.currentToken() != JsonToken.END_OBJECT) { + return context.reportInputMismatch( + ExampleDefensiveCollectionSetsUnion.class, + "Expected the end of a JSON object while deserializing a union"); + } + return new ExampleDefensiveCollectionSetsUnion(new UnknownWrapper(type, values)); + } + } } diff --git a/conjure-java-core/src/integrationInput/java/dialogue/com/palantir/product/AnyExample.java b/conjure-java-core/src/integrationInput/java/dialogue/com/palantir/product/AnyExample.java index 02449329c..1804f54d2 100644 --- a/conjure-java-core/src/integrationInput/java/dialogue/com/palantir/product/AnyExample.java +++ b/conjure-java-core/src/integrationInput/java/dialogue/com/palantir/product/AnyExample.java @@ -111,6 +111,7 @@ public static final class Builder { private Object anyValue; + @JsonSetter(value = "anyMap", nulls = Nulls.SKIP) private Map anyMap = new LinkedHashMap<>(); private Builder() {} @@ -129,7 +130,6 @@ public Builder anyValue(@Nonnull Object anyValue) { return this; } - @JsonSetter(value = "anyMap", nulls = Nulls.SKIP) public Builder anyMap(@Nonnull Map anyMap) { checkNotBuilt(); this.anyMap = new LinkedHashMap<>(Preconditions.checkNotNull(anyMap, "anyMap cannot be null")); diff --git a/conjure-java-core/src/integrationInput/java/dialogue/com/palantir/product/CollectionExample.java b/conjure-java-core/src/integrationInput/java/dialogue/com/palantir/product/CollectionExample.java index 120decf47..fc338a4eb 100644 --- a/conjure-java-core/src/integrationInput/java/dialogue/com/palantir/product/CollectionExample.java +++ b/conjure-java-core/src/integrationInput/java/dialogue/com/palantir/product/CollectionExample.java @@ -132,6 +132,7 @@ public static final class Builder { private Set stringSet = ConjureCollections.newSet(); + @JsonSetter(value = "stringMap", nulls = Nulls.SKIP) private Map stringMap = new LinkedHashMap<>(); private Builder() {} @@ -186,7 +187,6 @@ public Builder stringSet(String stringSet) { return this; } - @JsonSetter(value = "stringMap", nulls = Nulls.SKIP) public Builder stringMap(@Nonnull Map stringMap) { checkNotBuilt(); this.stringMap = new LinkedHashMap<>(Preconditions.checkNotNull(stringMap, "stringMap cannot be null")); diff --git a/conjure-java-core/src/integrationInput/java/dialogue/com/palantir/product/ComplexExample.java b/conjure-java-core/src/integrationInput/java/dialogue/com/palantir/product/ComplexExample.java index 590b5797b..a6d63bfea 100644 --- a/conjure-java-core/src/integrationInput/java/dialogue/com/palantir/product/ComplexExample.java +++ b/conjure-java-core/src/integrationInput/java/dialogue/com/palantir/product/ComplexExample.java @@ -140,6 +140,7 @@ public static Builder builder() { public static final class Builder { boolean _buildInvoked; + @JsonSetter(value = "metadata", nulls = Nulls.SKIP, contentNulls = Nulls.AS_EMPTY) private Map>> metadata = new LinkedHashMap<>(); private EnumExample status; @@ -159,7 +160,6 @@ public Builder from(ComplexExample other) { return this; } - @JsonSetter(value = "metadata", nulls = Nulls.SKIP, contentNulls = Nulls.AS_EMPTY) public Builder metadata(@Nonnull Map>> metadata) { checkNotBuilt(); this.metadata = new LinkedHashMap<>(Preconditions.checkNotNull(metadata, "metadata cannot be null")); diff --git a/conjure-java-core/src/integrationInput/java/dialogue/com/palantir/product/EmptyUnion.java b/conjure-java-core/src/integrationInput/java/dialogue/com/palantir/product/EmptyUnion.java index f5316d77b..6480036b0 100644 --- a/conjure-java-core/src/integrationInput/java/dialogue/com/palantir/product/EmptyUnion.java +++ b/conjure-java-core/src/integrationInput/java/dialogue/com/palantir/product/EmptyUnion.java @@ -5,12 +5,25 @@ import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.annotation.JsonSubTypes; -import com.fasterxml.jackson.annotation.JsonTypeInfo; +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.JsonToken; +import com.fasterxml.jackson.core.util.JsonParserSequence; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonDeserializer; +import com.fasterxml.jackson.databind.JsonMappingException; +import com.fasterxml.jackson.databind.JsonSerializer; +import com.fasterxml.jackson.databind.MapperFeature; +import com.fasterxml.jackson.databind.SerializerProvider; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import com.fasterxml.jackson.databind.deser.ResolvableDeserializer; +import com.fasterxml.jackson.databind.util.TokenBuffer; import com.palantir.logsafe.Preconditions; import com.palantir.logsafe.Safe; import com.palantir.logsafe.SafeArg; import com.palantir.logsafe.exceptions.SafeIllegalArgumentException; +import java.io.IOException; import java.util.Collections; import java.util.HashMap; import java.util.Map; @@ -20,13 +33,8 @@ import javax.annotation.processing.Generated; @Generated("com.palantir.conjure.java.types.UnionGenerator") -@JsonTypeInfo( - use = JsonTypeInfo.Id.NAME, - include = JsonTypeInfo.As.EXISTING_PROPERTY, - property = "type", - visible = true, - defaultImpl = EmptyUnion.Unknown.class) -@JsonSubTypes({}) +@JsonDeserialize(using = EmptyUnion.Deserializer.class) +@JsonSerialize(using = EmptyUnion.Serializer.class) @JsonIgnoreProperties(ignoreUnknown = true) public abstract sealed class EmptyUnion permits EmptyUnion.Unknown { public static EmptyUnion unknown(@Safe String type, Object value) { @@ -38,6 +46,8 @@ public static EmptyUnion unknown(@Safe String type, Object value) { public abstract T accept(Visitor visitor); + @JsonDeserialize + @JsonSerialize public static final class Unknown extends EmptyUnion { private final String type; @@ -98,6 +108,134 @@ public String toString() { } } + static final class Serializer extends JsonSerializer { + @Override + public void serialize(EmptyUnion value, JsonGenerator generator, SerializerProvider serializers) + throws IOException { + serializers.findValueSerializer(value.getClass()).serialize(value, generator, serializers); + } + } + + static final class Deserializer extends JsonDeserializer implements ResolvableDeserializer { + private static final Class[] VARIANT_TYPES = new Class[] {}; + + private volatile JsonDeserializer[] deserializers; + + @Override + public void resolve(DeserializationContext context) throws JsonMappingException { + deserializers = new JsonDeserializer[VARIANT_TYPES.length]; + } + + @Override + public boolean isCachable() { + return true; + } + + @Override + public EmptyUnion deserialize(JsonParser parser, DeserializationContext context) throws IOException { + if (!parser.isExpectedStartObjectToken()) { + return context.reportInputMismatch( + EmptyUnion.class, "Expected a JSON object for union deserialization"); + } + JsonToken firstToken = parser.nextToken(); + if (firstToken == JsonToken.FIELD_NAME && isTypeField(parser.currentName(), context)) { + if (parser.nextToken() != JsonToken.VALUE_STRING) { + return context.reportInputMismatch(EmptyUnion.class, "Union discriminator 'type' must be a string"); + } + String type = parser.getText(); + parser.nextToken(); + return deserializeSelected(parser, context, type); + } + return deserializeBuffered(parser, context); + } + + private EmptyUnion deserializeBuffered(JsonParser parser, DeserializationContext context) throws IOException { + try (TokenBuffer buffer = context.bufferForInputBuffering(parser)) { + buffer.writeStartObject(); + JsonToken token = parser.currentToken(); + while (token == JsonToken.FIELD_NAME) { + String fieldName = parser.currentName(); + JsonToken valueToken = parser.nextToken(); + if (isTypeField(fieldName, context)) { + if (valueToken != JsonToken.VALUE_STRING) { + return context.reportInputMismatch( + EmptyUnion.class, "Union discriminator 'type' must be a string"); + } + String type = parser.getText(); + parser.nextToken(); + try (JsonParser bufferedParser = buffer.asParser(parser)) { + JsonParser combinedParser = + JsonParserSequence.createFlattened(true, bufferedParser, parser); + combinedParser.nextToken(); + return deserializeSelected(combinedParser, context, type); + } + } + buffer.writeFieldName(fieldName); + buffer.copyCurrentStructure(parser); + token = parser.nextToken(); + } + if (token != JsonToken.END_OBJECT) { + return context.reportInputMismatch( + EmptyUnion.class, "Expected the end of a JSON object while deserializing a union"); + } + } + return context.reportInputMismatch(EmptyUnion.class, "Union discriminator 'type' is required"); + } + + private static boolean isTypeField(String fieldName, DeserializationContext context) { + return "type".equals(fieldName) + || (context.isEnabled(MapperFeature.ACCEPT_CASE_INSENSITIVE_PROPERTIES) + && "type".equalsIgnoreCase(fieldName)); + } + + private EmptyUnion deserializeSelected(JsonParser parser, DeserializationContext context, String type) + throws IOException { + int variantIndex = + switch (type) { + default -> -1; + }; + if (variantIndex < 0) { + return deserializeUnknown(parser, context, type); + } + JsonDeserializer deserializer = deserializers[variantIndex]; + if (deserializer == null) { + deserializer = resolveDeserializer(context, variantIndex); + } + return (EmptyUnion) deserializer.deserialize(parser, context); + } + + private synchronized JsonDeserializer resolveDeserializer(DeserializationContext context, int variantIndex) + throws JsonMappingException { + JsonDeserializer deserializer = deserializers[variantIndex]; + if (deserializer == null) { + deserializer = context.findRootValueDeserializer(context.constructType(VARIANT_TYPES[variantIndex])); + JsonDeserializer[] updated = deserializers.clone(); + updated[variantIndex] = deserializer; + deserializers = updated; + } + return deserializer; + } + + private static EmptyUnion deserializeUnknown(JsonParser parser, DeserializationContext context, String type) + throws IOException { + Map values = new HashMap<>(); + if (parser.currentToken() == JsonToken.START_OBJECT) { + parser.nextToken(); + } + while (parser.currentToken() == JsonToken.FIELD_NAME) { + String fieldName = parser.currentName(); + parser.nextToken(); + values.put(fieldName, context.readValue(parser, Object.class)); + parser.nextToken(); + } + if (parser.currentToken() != JsonToken.END_OBJECT) { + return context.reportInputMismatch( + EmptyUnion.class, "Expected the end of a JSON object while deserializing a union"); + } + return new Unknown(type, values); + } + } + public interface Visitor { T visitUnknown(@Safe String unknownType); diff --git a/conjure-java-core/src/integrationInput/java/dialogue/com/palantir/product/NestedCollectionExample.java b/conjure-java-core/src/integrationInput/java/dialogue/com/palantir/product/NestedCollectionExample.java index ca396af10..249fa54ec 100644 --- a/conjure-java-core/src/integrationInput/java/dialogue/com/palantir/product/NestedCollectionExample.java +++ b/conjure-java-core/src/integrationInput/java/dialogue/com/palantir/product/NestedCollectionExample.java @@ -138,8 +138,10 @@ public static final class Builder { private List> nestedList = ConjureCollections.newList(); + @JsonSetter(value = "nestedMap", nulls = Nulls.SKIP) private Map> nestedMap = new LinkedHashMap<>(); + @JsonSetter(value = "mixedCollection", nulls = Nulls.SKIP) private Map> mixedCollection = new LinkedHashMap<>(); private Builder() {} @@ -173,7 +175,6 @@ public Builder nestedList(List nestedList) { return this; } - @JsonSetter(value = "nestedMap", nulls = Nulls.SKIP) public Builder nestedMap(@Nonnull Map> nestedMap) { checkNotBuilt(); this.nestedMap = new LinkedHashMap<>(Preconditions.checkNotNull(nestedMap, "nestedMap cannot be null")); @@ -192,7 +193,6 @@ public Builder nestedMap(String key, Map value) { return this; } - @JsonSetter(value = "mixedCollection", nulls = Nulls.SKIP) public Builder mixedCollection(@Nonnull Map> mixedCollection) { checkNotBuilt(); this.mixedCollection = diff --git a/conjure-java-core/src/integrationInput/java/dialogue/com/palantir/product/SimpleUnion.java b/conjure-java-core/src/integrationInput/java/dialogue/com/palantir/product/SimpleUnion.java index 757a4fc4d..72905bc1d 100644 --- a/conjure-java-core/src/integrationInput/java/dialogue/com/palantir/product/SimpleUnion.java +++ b/conjure-java-core/src/integrationInput/java/dialogue/com/palantir/product/SimpleUnion.java @@ -6,14 +6,27 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.annotation.JsonSetter; -import com.fasterxml.jackson.annotation.JsonSubTypes; -import com.fasterxml.jackson.annotation.JsonTypeInfo; import com.fasterxml.jackson.annotation.JsonTypeName; +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.JsonToken; +import com.fasterxml.jackson.core.util.JsonParserSequence; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonDeserializer; +import com.fasterxml.jackson.databind.JsonMappingException; +import com.fasterxml.jackson.databind.JsonSerializer; +import com.fasterxml.jackson.databind.MapperFeature; +import com.fasterxml.jackson.databind.SerializerProvider; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import com.fasterxml.jackson.databind.deser.ResolvableDeserializer; +import com.fasterxml.jackson.databind.util.TokenBuffer; import com.palantir.conjure.java.lib.SafeLong; import com.palantir.logsafe.Preconditions; import com.palantir.logsafe.Safe; import com.palantir.logsafe.SafeArg; import com.palantir.logsafe.exceptions.SafeIllegalArgumentException; +import java.io.IOException; import java.util.Collections; import java.util.HashMap; import java.util.Map; @@ -24,17 +37,8 @@ import javax.annotation.processing.Generated; @Generated("com.palantir.conjure.java.types.UnionGenerator") -@JsonTypeInfo( - use = JsonTypeInfo.Id.NAME, - include = JsonTypeInfo.As.EXISTING_PROPERTY, - property = "type", - visible = true, - defaultImpl = SimpleUnion.Unknown.class) -@JsonSubTypes({ - @JsonSubTypes.Type(value = SimpleUnion.Foo.class, name = "foo"), - @JsonSubTypes.Type(value = SimpleUnion.Bar.class, name = "bar"), - @JsonSubTypes.Type(value = SimpleUnion.Baz.class, name = "baz") -}) +@JsonDeserialize(using = SimpleUnion.Deserializer.class) +@JsonSerialize(using = SimpleUnion.Serializer.class) @JsonIgnoreProperties(ignoreUnknown = true) public abstract sealed class SimpleUnion permits SimpleUnion.Foo, SimpleUnion.Bar, SimpleUnion.Baz, SimpleUnion.Unknown { @@ -80,6 +84,9 @@ public Known throwOnUnknown() { public sealed interface Known permits Foo, Bar, Baz {} @JsonTypeName("foo") + @JsonIgnoreProperties(ignoreUnknown = true) + @JsonDeserialize + @JsonSerialize public static final class Foo extends SimpleUnion implements Known { private final String value; @@ -125,6 +132,9 @@ public String toString() { } @JsonTypeName("bar") + @JsonIgnoreProperties(ignoreUnknown = true) + @JsonDeserialize + @JsonSerialize public static final class Bar extends SimpleUnion implements Known { private final int value; @@ -170,6 +180,9 @@ public String toString() { } @JsonTypeName("baz") + @JsonIgnoreProperties(ignoreUnknown = true) + @JsonDeserialize + @JsonSerialize public static final class Baz extends SimpleUnion implements Known { private final SafeLong value; @@ -214,6 +227,8 @@ public String toString() { } } + @JsonDeserialize + @JsonSerialize public static final class Unknown extends SimpleUnion { private final String type; @@ -274,6 +289,138 @@ public String toString() { } } + static final class Serializer extends JsonSerializer { + @Override + public void serialize(SimpleUnion value, JsonGenerator generator, SerializerProvider serializers) + throws IOException { + serializers.findValueSerializer(value.getClass()).serialize(value, generator, serializers); + } + } + + static final class Deserializer extends JsonDeserializer implements ResolvableDeserializer { + private static final Class[] VARIANT_TYPES = new Class[] {Foo.class, Bar.class, Baz.class}; + + private volatile JsonDeserializer[] deserializers; + + @Override + public void resolve(DeserializationContext context) throws JsonMappingException { + deserializers = new JsonDeserializer[VARIANT_TYPES.length]; + } + + @Override + public boolean isCachable() { + return true; + } + + @Override + public SimpleUnion deserialize(JsonParser parser, DeserializationContext context) throws IOException { + if (!parser.isExpectedStartObjectToken()) { + return context.reportInputMismatch( + SimpleUnion.class, "Expected a JSON object for union deserialization"); + } + JsonToken firstToken = parser.nextToken(); + if (firstToken == JsonToken.FIELD_NAME && isTypeField(parser.currentName(), context)) { + if (parser.nextToken() != JsonToken.VALUE_STRING) { + return context.reportInputMismatch( + SimpleUnion.class, "Union discriminator 'type' must be a string"); + } + String type = parser.getText(); + parser.nextToken(); + return deserializeSelected(parser, context, type); + } + return deserializeBuffered(parser, context); + } + + private SimpleUnion deserializeBuffered(JsonParser parser, DeserializationContext context) throws IOException { + try (TokenBuffer buffer = context.bufferForInputBuffering(parser)) { + buffer.writeStartObject(); + JsonToken token = parser.currentToken(); + while (token == JsonToken.FIELD_NAME) { + String fieldName = parser.currentName(); + JsonToken valueToken = parser.nextToken(); + if (isTypeField(fieldName, context)) { + if (valueToken != JsonToken.VALUE_STRING) { + return context.reportInputMismatch( + SimpleUnion.class, "Union discriminator 'type' must be a string"); + } + String type = parser.getText(); + parser.nextToken(); + try (JsonParser bufferedParser = buffer.asParser(parser)) { + JsonParser combinedParser = + JsonParserSequence.createFlattened(true, bufferedParser, parser); + combinedParser.nextToken(); + return deserializeSelected(combinedParser, context, type); + } + } + buffer.writeFieldName(fieldName); + buffer.copyCurrentStructure(parser); + token = parser.nextToken(); + } + if (token != JsonToken.END_OBJECT) { + return context.reportInputMismatch( + SimpleUnion.class, "Expected the end of a JSON object while deserializing a union"); + } + } + return context.reportInputMismatch(SimpleUnion.class, "Union discriminator 'type' is required"); + } + + private static boolean isTypeField(String fieldName, DeserializationContext context) { + return "type".equals(fieldName) + || (context.isEnabled(MapperFeature.ACCEPT_CASE_INSENSITIVE_PROPERTIES) + && "type".equalsIgnoreCase(fieldName)); + } + + private SimpleUnion deserializeSelected(JsonParser parser, DeserializationContext context, String type) + throws IOException { + int variantIndex = + switch (type) { + case "foo" -> 0; + case "bar" -> 1; + case "baz" -> 2; + default -> -1; + }; + if (variantIndex < 0) { + return deserializeUnknown(parser, context, type); + } + JsonDeserializer deserializer = deserializers[variantIndex]; + if (deserializer == null) { + deserializer = resolveDeserializer(context, variantIndex); + } + return (SimpleUnion) deserializer.deserialize(parser, context); + } + + private synchronized JsonDeserializer resolveDeserializer(DeserializationContext context, int variantIndex) + throws JsonMappingException { + JsonDeserializer deserializer = deserializers[variantIndex]; + if (deserializer == null) { + deserializer = context.findRootValueDeserializer(context.constructType(VARIANT_TYPES[variantIndex])); + JsonDeserializer[] updated = deserializers.clone(); + updated[variantIndex] = deserializer; + deserializers = updated; + } + return deserializer; + } + + private static SimpleUnion deserializeUnknown(JsonParser parser, DeserializationContext context, String type) + throws IOException { + Map values = new HashMap<>(); + if (parser.currentToken() == JsonToken.START_OBJECT) { + parser.nextToken(); + } + while (parser.currentToken() == JsonToken.FIELD_NAME) { + String fieldName = parser.currentName(); + parser.nextToken(); + values.put(fieldName, context.readValue(parser, Object.class)); + parser.nextToken(); + } + if (parser.currentToken() != JsonToken.END_OBJECT) { + return context.reportInputMismatch( + SimpleUnion.class, "Expected the end of a JSON object while deserializing a union"); + } + return new Unknown(type, values); + } + } + public interface Visitor { T visitFoo(String value); diff --git a/conjure-java-core/src/integrationInput/java/dialogue/com/palantir/product/UnionExample.java b/conjure-java-core/src/integrationInput/java/dialogue/com/palantir/product/UnionExample.java index 6a277abc2..b78cdfb19 100644 --- a/conjure-java-core/src/integrationInput/java/dialogue/com/palantir/product/UnionExample.java +++ b/conjure-java-core/src/integrationInput/java/dialogue/com/palantir/product/UnionExample.java @@ -6,14 +6,27 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.annotation.JsonSetter; -import com.fasterxml.jackson.annotation.JsonSubTypes; -import com.fasterxml.jackson.annotation.JsonTypeInfo; import com.fasterxml.jackson.annotation.JsonTypeName; import com.fasterxml.jackson.annotation.Nulls; +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.JsonToken; +import com.fasterxml.jackson.core.util.JsonParserSequence; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonDeserializer; +import com.fasterxml.jackson.databind.JsonMappingException; +import com.fasterxml.jackson.databind.JsonSerializer; +import com.fasterxml.jackson.databind.MapperFeature; +import com.fasterxml.jackson.databind.SerializerProvider; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import com.fasterxml.jackson.databind.deser.ResolvableDeserializer; +import com.fasterxml.jackson.databind.util.TokenBuffer; import com.palantir.logsafe.Preconditions; import com.palantir.logsafe.Safe; import com.palantir.logsafe.SafeArg; import com.palantir.logsafe.exceptions.SafeIllegalArgumentException; +import java.io.IOException; import java.util.Collections; import java.util.HashMap; import java.util.List; @@ -26,19 +39,8 @@ import javax.annotation.processing.Generated; @Generated("com.palantir.conjure.java.types.UnionGenerator") -@JsonTypeInfo( - use = JsonTypeInfo.Id.NAME, - include = JsonTypeInfo.As.EXISTING_PROPERTY, - property = "type", - visible = true, - defaultImpl = UnionExample.Unknown.class) -@JsonSubTypes({ - @JsonSubTypes.Type(value = UnionExample.StringVariant.class, name = "stringVariant"), - @JsonSubTypes.Type(value = UnionExample.IntVariant.class, name = "intVariant"), - @JsonSubTypes.Type(value = UnionExample.ObjectVariant.class, name = "objectVariant"), - @JsonSubTypes.Type(value = UnionExample.CollectionVariant.class, name = "collectionVariant"), - @JsonSubTypes.Type(value = UnionExample.OptionalVariant.class, name = "optionalVariant") -}) +@JsonDeserialize(using = UnionExample.Deserializer.class) +@JsonSerialize(using = UnionExample.Serializer.class) @JsonIgnoreProperties(ignoreUnknown = true) public abstract sealed class UnionExample permits UnionExample.StringVariant, @@ -104,6 +106,9 @@ public sealed interface Known permits StringVariant, IntVariant, ObjectVariant, CollectionVariant, OptionalVariant {} @JsonTypeName("stringVariant") + @JsonIgnoreProperties(ignoreUnknown = true) + @JsonDeserialize + @JsonSerialize public static final class StringVariant extends UnionExample implements Known { private final String value; @@ -149,6 +154,9 @@ public String toString() { } @JsonTypeName("intVariant") + @JsonIgnoreProperties(ignoreUnknown = true) + @JsonDeserialize + @JsonSerialize public static final class IntVariant extends UnionExample implements Known { private final int value; @@ -194,6 +202,9 @@ public String toString() { } @JsonTypeName("objectVariant") + @JsonIgnoreProperties(ignoreUnknown = true) + @JsonDeserialize + @JsonSerialize public static final class ObjectVariant extends UnionExample implements Known { private final ObjectReference value; @@ -239,6 +250,9 @@ public String toString() { } @JsonTypeName("collectionVariant") + @JsonIgnoreProperties(ignoreUnknown = true) + @JsonDeserialize + @JsonSerialize public static final class CollectionVariant extends UnionExample implements Known { private final List value; @@ -285,6 +299,9 @@ public String toString() { } @JsonTypeName("optionalVariant") + @JsonIgnoreProperties(ignoreUnknown = true) + @JsonDeserialize + @JsonSerialize public static final class OptionalVariant extends UnionExample implements Known { private final Optional value; @@ -330,6 +347,8 @@ public String toString() { } } + @JsonDeserialize + @JsonSerialize public static final class Unknown extends UnionExample { private final String type; @@ -390,6 +409,142 @@ public String toString() { } } + static final class Serializer extends JsonSerializer { + @Override + public void serialize(UnionExample value, JsonGenerator generator, SerializerProvider serializers) + throws IOException { + serializers.findValueSerializer(value.getClass()).serialize(value, generator, serializers); + } + } + + static final class Deserializer extends JsonDeserializer implements ResolvableDeserializer { + private static final Class[] VARIANT_TYPES = new Class[] { + StringVariant.class, IntVariant.class, ObjectVariant.class, CollectionVariant.class, OptionalVariant.class + }; + + private volatile JsonDeserializer[] deserializers; + + @Override + public void resolve(DeserializationContext context) throws JsonMappingException { + deserializers = new JsonDeserializer[VARIANT_TYPES.length]; + } + + @Override + public boolean isCachable() { + return true; + } + + @Override + public UnionExample deserialize(JsonParser parser, DeserializationContext context) throws IOException { + if (!parser.isExpectedStartObjectToken()) { + return context.reportInputMismatch( + UnionExample.class, "Expected a JSON object for union deserialization"); + } + JsonToken firstToken = parser.nextToken(); + if (firstToken == JsonToken.FIELD_NAME && isTypeField(parser.currentName(), context)) { + if (parser.nextToken() != JsonToken.VALUE_STRING) { + return context.reportInputMismatch( + UnionExample.class, "Union discriminator 'type' must be a string"); + } + String type = parser.getText(); + parser.nextToken(); + return deserializeSelected(parser, context, type); + } + return deserializeBuffered(parser, context); + } + + private UnionExample deserializeBuffered(JsonParser parser, DeserializationContext context) throws IOException { + try (TokenBuffer buffer = context.bufferForInputBuffering(parser)) { + buffer.writeStartObject(); + JsonToken token = parser.currentToken(); + while (token == JsonToken.FIELD_NAME) { + String fieldName = parser.currentName(); + JsonToken valueToken = parser.nextToken(); + if (isTypeField(fieldName, context)) { + if (valueToken != JsonToken.VALUE_STRING) { + return context.reportInputMismatch( + UnionExample.class, "Union discriminator 'type' must be a string"); + } + String type = parser.getText(); + parser.nextToken(); + try (JsonParser bufferedParser = buffer.asParser(parser)) { + JsonParser combinedParser = + JsonParserSequence.createFlattened(true, bufferedParser, parser); + combinedParser.nextToken(); + return deserializeSelected(combinedParser, context, type); + } + } + buffer.writeFieldName(fieldName); + buffer.copyCurrentStructure(parser); + token = parser.nextToken(); + } + if (token != JsonToken.END_OBJECT) { + return context.reportInputMismatch( + UnionExample.class, "Expected the end of a JSON object while deserializing a union"); + } + } + return context.reportInputMismatch(UnionExample.class, "Union discriminator 'type' is required"); + } + + private static boolean isTypeField(String fieldName, DeserializationContext context) { + return "type".equals(fieldName) + || (context.isEnabled(MapperFeature.ACCEPT_CASE_INSENSITIVE_PROPERTIES) + && "type".equalsIgnoreCase(fieldName)); + } + + private UnionExample deserializeSelected(JsonParser parser, DeserializationContext context, String type) + throws IOException { + int variantIndex = + switch (type) { + case "stringVariant" -> 0; + case "intVariant" -> 1; + case "objectVariant" -> 2; + case "collectionVariant" -> 3; + case "optionalVariant" -> 4; + default -> -1; + }; + if (variantIndex < 0) { + return deserializeUnknown(parser, context, type); + } + JsonDeserializer deserializer = deserializers[variantIndex]; + if (deserializer == null) { + deserializer = resolveDeserializer(context, variantIndex); + } + return (UnionExample) deserializer.deserialize(parser, context); + } + + private synchronized JsonDeserializer resolveDeserializer(DeserializationContext context, int variantIndex) + throws JsonMappingException { + JsonDeserializer deserializer = deserializers[variantIndex]; + if (deserializer == null) { + deserializer = context.findRootValueDeserializer(context.constructType(VARIANT_TYPES[variantIndex])); + JsonDeserializer[] updated = deserializers.clone(); + updated[variantIndex] = deserializer; + deserializers = updated; + } + return deserializer; + } + + private static UnionExample deserializeUnknown(JsonParser parser, DeserializationContext context, String type) + throws IOException { + Map values = new HashMap<>(); + if (parser.currentToken() == JsonToken.START_OBJECT) { + parser.nextToken(); + } + while (parser.currentToken() == JsonToken.FIELD_NAME) { + String fieldName = parser.currentName(); + parser.nextToken(); + values.put(fieldName, context.readValue(parser, Object.class)); + parser.nextToken(); + } + if (parser.currentToken() != JsonToken.END_OBJECT) { + return context.reportInputMismatch( + UnionExample.class, "Expected the end of a JSON object while deserializing a union"); + } + return new Unknown(type, values); + } + } + public interface Visitor { T visitStringVariant(String value); diff --git a/conjure-java-core/src/integrationInput/java/errors/com/palantir/product/AnyExample.java b/conjure-java-core/src/integrationInput/java/errors/com/palantir/product/AnyExample.java index 6aef01025..ed6c5dd13 100644 --- a/conjure-java-core/src/integrationInput/java/errors/com/palantir/product/AnyExample.java +++ b/conjure-java-core/src/integrationInput/java/errors/com/palantir/product/AnyExample.java @@ -111,6 +111,7 @@ public static final class Builder { private Object anyValue; + @JsonSetter(value = "anyMap", nulls = Nulls.SKIP) private Map anyMap = new LinkedHashMap<>(); private Builder() {} @@ -129,7 +130,6 @@ public Builder anyValue(@Nonnull Object anyValue) { return this; } - @JsonSetter(value = "anyMap", nulls = Nulls.SKIP) public Builder anyMap(@Nonnull Map anyMap) { checkNotBuilt(); this.anyMap = new LinkedHashMap<>(Preconditions.checkNotNull(anyMap, "anyMap cannot be null")); diff --git a/conjure-java-core/src/integrationInput/java/errors/com/palantir/product/CollectionExample.java b/conjure-java-core/src/integrationInput/java/errors/com/palantir/product/CollectionExample.java index 03ff4708e..dc14b9ca8 100644 --- a/conjure-java-core/src/integrationInput/java/errors/com/palantir/product/CollectionExample.java +++ b/conjure-java-core/src/integrationInput/java/errors/com/palantir/product/CollectionExample.java @@ -132,6 +132,7 @@ public static final class Builder { private Set stringSet = ConjureCollections.newSet(); + @JsonSetter(value = "stringMap", nulls = Nulls.SKIP) private Map stringMap = new LinkedHashMap<>(); private Builder() {} @@ -186,7 +187,6 @@ public Builder stringSet(String stringSet) { return this; } - @JsonSetter(value = "stringMap", nulls = Nulls.SKIP) public Builder stringMap(@Nonnull Map stringMap) { checkNotBuilt(); this.stringMap = new LinkedHashMap<>(Preconditions.checkNotNull(stringMap, "stringMap cannot be null")); diff --git a/conjure-java-core/src/integrationInput/java/errors/com/palantir/product/ComplexExample.java b/conjure-java-core/src/integrationInput/java/errors/com/palantir/product/ComplexExample.java index b80256a27..ed83f0bcc 100644 --- a/conjure-java-core/src/integrationInput/java/errors/com/palantir/product/ComplexExample.java +++ b/conjure-java-core/src/integrationInput/java/errors/com/palantir/product/ComplexExample.java @@ -142,6 +142,7 @@ public static Builder builder() { public static final class Builder { boolean _buildInvoked; + @JsonSetter(value = "metadata", nulls = Nulls.SKIP, contentNulls = Nulls.AS_EMPTY) private Map>> metadata = new LinkedHashMap<>(); private EnumExample status; @@ -161,7 +162,6 @@ public Builder from(ComplexExample other) { return this; } - @JsonSetter(value = "metadata", nulls = Nulls.SKIP, contentNulls = Nulls.AS_EMPTY) public Builder metadata(@Nonnull Map>> metadata) { checkNotBuilt(); this.metadata = new LinkedHashMap<>(Preconditions.checkNotNull(metadata, "metadata cannot be null")); diff --git a/conjure-java-core/src/integrationInput/java/errors/com/palantir/product/EmptyUnion.java b/conjure-java-core/src/integrationInput/java/errors/com/palantir/product/EmptyUnion.java index 1e8079a97..18ca505dc 100644 --- a/conjure-java-core/src/integrationInput/java/errors/com/palantir/product/EmptyUnion.java +++ b/conjure-java-core/src/integrationInput/java/errors/com/palantir/product/EmptyUnion.java @@ -3,14 +3,23 @@ import com.fasterxml.jackson.annotation.JsonAnyGetter; import com.fasterxml.jackson.annotation.JsonAnySetter; import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.annotation.JsonTypeInfo; import com.fasterxml.jackson.annotation.JsonValue; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.JsonToken; +import com.fasterxml.jackson.core.util.JsonParserSequence; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonDeserializer; +import com.fasterxml.jackson.databind.JsonMappingException; +import com.fasterxml.jackson.databind.MapperFeature; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.deser.ResolvableDeserializer; +import com.fasterxml.jackson.databind.util.TokenBuffer; import com.palantir.logsafe.Preconditions; import com.palantir.logsafe.Safe; import com.palantir.logsafe.SafeArg; import com.palantir.logsafe.exceptions.SafeIllegalArgumentException; +import java.io.IOException; import java.util.Collections; import java.util.HashMap; import java.util.Map; @@ -20,6 +29,7 @@ import javax.annotation.processing.Generated; @Generated("com.palantir.conjure.java.types.UnionGenerator") +@JsonDeserialize(using = EmptyUnion.Deserializer.class) public final class EmptyUnion { private final Base value; @@ -113,13 +123,6 @@ public interface Completed_StageVisitorBuilder { Visitor build(); } - @JsonTypeInfo( - use = JsonTypeInfo.Id.NAME, - include = JsonTypeInfo.As.EXISTING_PROPERTY, - property = "type", - visible = true, - defaultImpl = UnknownWrapper.class) - @JsonIgnoreProperties(ignoreUnknown = true) private interface Base { T accept(Visitor visitor); } @@ -183,4 +186,124 @@ public String toString() { return "UnknownWrapper{type: " + type + ", value: " + value + '}'; } } + + static final class Deserializer extends JsonDeserializer implements ResolvableDeserializer { + private static final Class[] VARIANT_TYPES = new Class[] {}; + + private volatile JsonDeserializer[] deserializers; + + @Override + public void resolve(DeserializationContext context) throws JsonMappingException { + deserializers = new JsonDeserializer[VARIANT_TYPES.length]; + } + + @Override + public boolean isCachable() { + return true; + } + + @Override + public EmptyUnion deserialize(JsonParser parser, DeserializationContext context) throws IOException { + if (!parser.isExpectedStartObjectToken()) { + return context.reportInputMismatch( + EmptyUnion.class, "Expected a JSON object for union deserialization"); + } + JsonToken firstToken = parser.nextToken(); + if (firstToken == JsonToken.FIELD_NAME && isTypeField(parser.currentName(), context)) { + if (parser.nextToken() != JsonToken.VALUE_STRING) { + return context.reportInputMismatch(EmptyUnion.class, "Union discriminator 'type' must be a string"); + } + String type = parser.getText(); + parser.nextToken(); + return deserializeSelected(parser, context, type); + } + return deserializeBuffered(parser, context); + } + + private EmptyUnion deserializeBuffered(JsonParser parser, DeserializationContext context) throws IOException { + try (TokenBuffer buffer = context.bufferForInputBuffering(parser)) { + buffer.writeStartObject(); + JsonToken token = parser.currentToken(); + while (token == JsonToken.FIELD_NAME) { + String fieldName = parser.currentName(); + JsonToken valueToken = parser.nextToken(); + if (isTypeField(fieldName, context)) { + if (valueToken != JsonToken.VALUE_STRING) { + return context.reportInputMismatch( + EmptyUnion.class, "Union discriminator 'type' must be a string"); + } + String type = parser.getText(); + parser.nextToken(); + try (JsonParser bufferedParser = buffer.asParser(parser)) { + JsonParser combinedParser = + JsonParserSequence.createFlattened(true, bufferedParser, parser); + combinedParser.nextToken(); + return deserializeSelected(combinedParser, context, type); + } + } + buffer.writeFieldName(fieldName); + buffer.copyCurrentStructure(parser); + token = parser.nextToken(); + } + if (token != JsonToken.END_OBJECT) { + return context.reportInputMismatch( + EmptyUnion.class, "Expected the end of a JSON object while deserializing a union"); + } + } + return context.reportInputMismatch(EmptyUnion.class, "Union discriminator 'type' is required"); + } + + private static boolean isTypeField(String fieldName, DeserializationContext context) { + return "type".equals(fieldName) + || (context.isEnabled(MapperFeature.ACCEPT_CASE_INSENSITIVE_PROPERTIES) + && "type".equalsIgnoreCase(fieldName)); + } + + private EmptyUnion deserializeSelected(JsonParser parser, DeserializationContext context, String type) + throws IOException { + int variantIndex = + switch (type) { + default -> -1; + }; + if (variantIndex < 0) { + return deserializeUnknown(parser, context, type); + } + JsonDeserializer deserializer = deserializers[variantIndex]; + if (deserializer == null) { + deserializer = resolveDeserializer(context, variantIndex); + } + return new EmptyUnion((Base) deserializer.deserialize(parser, context)); + } + + private synchronized JsonDeserializer resolveDeserializer(DeserializationContext context, int variantIndex) + throws JsonMappingException { + JsonDeserializer deserializer = deserializers[variantIndex]; + if (deserializer == null) { + deserializer = context.findRootValueDeserializer(context.constructType(VARIANT_TYPES[variantIndex])); + JsonDeserializer[] updated = deserializers.clone(); + updated[variantIndex] = deserializer; + deserializers = updated; + } + return deserializer; + } + + private static EmptyUnion deserializeUnknown(JsonParser parser, DeserializationContext context, String type) + throws IOException { + Map values = new HashMap<>(); + if (parser.currentToken() == JsonToken.START_OBJECT) { + parser.nextToken(); + } + while (parser.currentToken() == JsonToken.FIELD_NAME) { + String fieldName = parser.currentName(); + parser.nextToken(); + values.put(fieldName, context.readValue(parser, Object.class)); + parser.nextToken(); + } + if (parser.currentToken() != JsonToken.END_OBJECT) { + return context.reportInputMismatch( + EmptyUnion.class, "Expected the end of a JSON object while deserializing a union"); + } + return new EmptyUnion(new UnknownWrapper(type, values)); + } + } } diff --git a/conjure-java-core/src/integrationInput/java/errors/com/palantir/product/NestedCollectionExample.java b/conjure-java-core/src/integrationInput/java/errors/com/palantir/product/NestedCollectionExample.java index 762c83bf4..bc6954cbe 100644 --- a/conjure-java-core/src/integrationInput/java/errors/com/palantir/product/NestedCollectionExample.java +++ b/conjure-java-core/src/integrationInput/java/errors/com/palantir/product/NestedCollectionExample.java @@ -138,8 +138,10 @@ public static final class Builder { private List> nestedList = ConjureCollections.newList(); + @JsonSetter(value = "nestedMap", nulls = Nulls.SKIP) private Map> nestedMap = new LinkedHashMap<>(); + @JsonSetter(value = "mixedCollection", nulls = Nulls.SKIP) private Map> mixedCollection = new LinkedHashMap<>(); private Builder() {} @@ -173,7 +175,6 @@ public Builder nestedList(List nestedList) { return this; } - @JsonSetter(value = "nestedMap", nulls = Nulls.SKIP) public Builder nestedMap(@Nonnull Map> nestedMap) { checkNotBuilt(); this.nestedMap = new LinkedHashMap<>(Preconditions.checkNotNull(nestedMap, "nestedMap cannot be null")); @@ -192,7 +193,6 @@ public Builder nestedMap(String key, Map value) { return this; } - @JsonSetter(value = "mixedCollection", nulls = Nulls.SKIP) public Builder mixedCollection(@Nonnull Map> mixedCollection) { checkNotBuilt(); this.mixedCollection = diff --git a/conjure-java-core/src/integrationInput/java/errors/com/palantir/product/UnionExample.java b/conjure-java-core/src/integrationInput/java/errors/com/palantir/product/UnionExample.java index 41bb4e582..d8844c6f1 100644 --- a/conjure-java-core/src/integrationInput/java/errors/com/palantir/product/UnionExample.java +++ b/conjure-java-core/src/integrationInput/java/errors/com/palantir/product/UnionExample.java @@ -6,15 +6,24 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.annotation.JsonSetter; -import com.fasterxml.jackson.annotation.JsonSubTypes; -import com.fasterxml.jackson.annotation.JsonTypeInfo; import com.fasterxml.jackson.annotation.JsonTypeName; import com.fasterxml.jackson.annotation.JsonValue; import com.fasterxml.jackson.annotation.Nulls; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.JsonToken; +import com.fasterxml.jackson.core.util.JsonParserSequence; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonDeserializer; +import com.fasterxml.jackson.databind.JsonMappingException; +import com.fasterxml.jackson.databind.MapperFeature; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.deser.ResolvableDeserializer; +import com.fasterxml.jackson.databind.util.TokenBuffer; import com.palantir.logsafe.Preconditions; import com.palantir.logsafe.Safe; import com.palantir.logsafe.SafeArg; import com.palantir.logsafe.exceptions.SafeIllegalArgumentException; +import java.io.IOException; import java.util.Collections; import java.util.HashMap; import java.util.List; @@ -27,6 +36,7 @@ import javax.annotation.processing.Generated; @Generated("com.palantir.conjure.java.types.UnionGenerator") +@JsonDeserialize(using = UnionExample.Deserializer.class) public final class UnionExample { private final Base value; @@ -270,25 +280,12 @@ public interface Completed_StageVisitorBuilder { Visitor build(); } - @JsonTypeInfo( - use = JsonTypeInfo.Id.NAME, - include = JsonTypeInfo.As.EXISTING_PROPERTY, - property = "type", - visible = true, - defaultImpl = UnknownWrapper.class) - @JsonSubTypes({ - @JsonSubTypes.Type(StringVariantWrapper.class), - @JsonSubTypes.Type(IntVariantWrapper.class), - @JsonSubTypes.Type(ObjectVariantWrapper.class), - @JsonSubTypes.Type(CollectionVariantWrapper.class), - @JsonSubTypes.Type(OptionalVariantWrapper.class) - }) - @JsonIgnoreProperties(ignoreUnknown = true) private interface Base { T accept(Visitor visitor); } @JsonTypeName("stringVariant") + @JsonIgnoreProperties(ignoreUnknown = true) private static final class StringVariantWrapper implements Base { private final String value; @@ -334,6 +331,7 @@ public String toString() { } @JsonTypeName("intVariant") + @JsonIgnoreProperties(ignoreUnknown = true) private static final class IntVariantWrapper implements Base { private final int value; @@ -379,6 +377,7 @@ public String toString() { } @JsonTypeName("objectVariant") + @JsonIgnoreProperties(ignoreUnknown = true) private static final class ObjectVariantWrapper implements Base { private final ObjectReference value; @@ -424,6 +423,7 @@ public String toString() { } @JsonTypeName("collectionVariant") + @JsonIgnoreProperties(ignoreUnknown = true) private static final class CollectionVariantWrapper implements Base { private final List value; @@ -471,6 +471,7 @@ public String toString() { } @JsonTypeName("optionalVariant") + @JsonIgnoreProperties(ignoreUnknown = true) private static final class OptionalVariantWrapper implements Base { private final Optional value; @@ -576,4 +577,136 @@ public String toString() { return "UnknownWrapper{type: " + type + ", value: " + value + '}'; } } + + static final class Deserializer extends JsonDeserializer implements ResolvableDeserializer { + private static final Class[] VARIANT_TYPES = new Class[] { + StringVariantWrapper.class, + IntVariantWrapper.class, + ObjectVariantWrapper.class, + CollectionVariantWrapper.class, + OptionalVariantWrapper.class + }; + + private volatile JsonDeserializer[] deserializers; + + @Override + public void resolve(DeserializationContext context) throws JsonMappingException { + deserializers = new JsonDeserializer[VARIANT_TYPES.length]; + } + + @Override + public boolean isCachable() { + return true; + } + + @Override + public UnionExample deserialize(JsonParser parser, DeserializationContext context) throws IOException { + if (!parser.isExpectedStartObjectToken()) { + return context.reportInputMismatch( + UnionExample.class, "Expected a JSON object for union deserialization"); + } + JsonToken firstToken = parser.nextToken(); + if (firstToken == JsonToken.FIELD_NAME && isTypeField(parser.currentName(), context)) { + if (parser.nextToken() != JsonToken.VALUE_STRING) { + return context.reportInputMismatch( + UnionExample.class, "Union discriminator 'type' must be a string"); + } + String type = parser.getText(); + parser.nextToken(); + return deserializeSelected(parser, context, type); + } + return deserializeBuffered(parser, context); + } + + private UnionExample deserializeBuffered(JsonParser parser, DeserializationContext context) throws IOException { + try (TokenBuffer buffer = context.bufferForInputBuffering(parser)) { + buffer.writeStartObject(); + JsonToken token = parser.currentToken(); + while (token == JsonToken.FIELD_NAME) { + String fieldName = parser.currentName(); + JsonToken valueToken = parser.nextToken(); + if (isTypeField(fieldName, context)) { + if (valueToken != JsonToken.VALUE_STRING) { + return context.reportInputMismatch( + UnionExample.class, "Union discriminator 'type' must be a string"); + } + String type = parser.getText(); + parser.nextToken(); + try (JsonParser bufferedParser = buffer.asParser(parser)) { + JsonParser combinedParser = + JsonParserSequence.createFlattened(true, bufferedParser, parser); + combinedParser.nextToken(); + return deserializeSelected(combinedParser, context, type); + } + } + buffer.writeFieldName(fieldName); + buffer.copyCurrentStructure(parser); + token = parser.nextToken(); + } + if (token != JsonToken.END_OBJECT) { + return context.reportInputMismatch( + UnionExample.class, "Expected the end of a JSON object while deserializing a union"); + } + } + return context.reportInputMismatch(UnionExample.class, "Union discriminator 'type' is required"); + } + + private static boolean isTypeField(String fieldName, DeserializationContext context) { + return "type".equals(fieldName) + || (context.isEnabled(MapperFeature.ACCEPT_CASE_INSENSITIVE_PROPERTIES) + && "type".equalsIgnoreCase(fieldName)); + } + + private UnionExample deserializeSelected(JsonParser parser, DeserializationContext context, String type) + throws IOException { + int variantIndex = + switch (type) { + case "stringVariant" -> 0; + case "intVariant" -> 1; + case "objectVariant" -> 2; + case "collectionVariant" -> 3; + case "optionalVariant" -> 4; + default -> -1; + }; + if (variantIndex < 0) { + return deserializeUnknown(parser, context, type); + } + JsonDeserializer deserializer = deserializers[variantIndex]; + if (deserializer == null) { + deserializer = resolveDeserializer(context, variantIndex); + } + return new UnionExample((Base) deserializer.deserialize(parser, context)); + } + + private synchronized JsonDeserializer resolveDeserializer(DeserializationContext context, int variantIndex) + throws JsonMappingException { + JsonDeserializer deserializer = deserializers[variantIndex]; + if (deserializer == null) { + deserializer = context.findRootValueDeserializer(context.constructType(VARIANT_TYPES[variantIndex])); + JsonDeserializer[] updated = deserializers.clone(); + updated[variantIndex] = deserializer; + deserializers = updated; + } + return deserializer; + } + + private static UnionExample deserializeUnknown(JsonParser parser, DeserializationContext context, String type) + throws IOException { + Map values = new HashMap<>(); + if (parser.currentToken() == JsonToken.START_OBJECT) { + parser.nextToken(); + } + while (parser.currentToken() == JsonToken.FIELD_NAME) { + String fieldName = parser.currentName(); + parser.nextToken(); + values.put(fieldName, context.readValue(parser, Object.class)); + parser.nextToken(); + } + if (parser.currentToken() != JsonToken.END_OBJECT) { + return context.reportInputMismatch( + UnionExample.class, "Expected the end of a JSON object while deserializing a union"); + } + return new UnionExample(new UnknownWrapper(type, values)); + } + } } diff --git a/conjure-java-core/src/integrationInput/java/exceptionthrowingdialogueinterfaces/com/palantir/product/AnyExample.java b/conjure-java-core/src/integrationInput/java/exceptionthrowingdialogueinterfaces/com/palantir/product/AnyExample.java index 0bd9fe5e0..267b3b889 100644 --- a/conjure-java-core/src/integrationInput/java/exceptionthrowingdialogueinterfaces/com/palantir/product/AnyExample.java +++ b/conjure-java-core/src/integrationInput/java/exceptionthrowingdialogueinterfaces/com/palantir/product/AnyExample.java @@ -111,6 +111,7 @@ public static final class Builder { private Object anyValue; + @JsonSetter(value = "anyMap", nulls = Nulls.SKIP) private Map anyMap = new LinkedHashMap<>(); private Builder() {} @@ -129,7 +130,6 @@ public Builder anyValue(@Nonnull Object anyValue) { return this; } - @JsonSetter(value = "anyMap", nulls = Nulls.SKIP) public Builder anyMap(@Nonnull Map anyMap) { checkNotBuilt(); this.anyMap = new LinkedHashMap<>(Preconditions.checkNotNull(anyMap, "anyMap cannot be null")); diff --git a/conjure-java-core/src/integrationInput/java/exceptionthrowingdialogueinterfaces/com/palantir/product/CollectionExample.java b/conjure-java-core/src/integrationInput/java/exceptionthrowingdialogueinterfaces/com/palantir/product/CollectionExample.java index 7848db1f8..ad84739aa 100644 --- a/conjure-java-core/src/integrationInput/java/exceptionthrowingdialogueinterfaces/com/palantir/product/CollectionExample.java +++ b/conjure-java-core/src/integrationInput/java/exceptionthrowingdialogueinterfaces/com/palantir/product/CollectionExample.java @@ -132,6 +132,7 @@ public static final class Builder { private Set stringSet = ConjureCollections.newSet(); + @JsonSetter(value = "stringMap", nulls = Nulls.SKIP) private Map stringMap = new LinkedHashMap<>(); private Builder() {} @@ -186,7 +187,6 @@ public Builder stringSet(String stringSet) { return this; } - @JsonSetter(value = "stringMap", nulls = Nulls.SKIP) public Builder stringMap(@Nonnull Map stringMap) { checkNotBuilt(); this.stringMap = new LinkedHashMap<>(Preconditions.checkNotNull(stringMap, "stringMap cannot be null")); diff --git a/conjure-java-core/src/integrationInput/java/exceptionthrowingdialogueinterfaces/com/palantir/product/ComplexExample.java b/conjure-java-core/src/integrationInput/java/exceptionthrowingdialogueinterfaces/com/palantir/product/ComplexExample.java index 54f883c49..3a8966203 100644 --- a/conjure-java-core/src/integrationInput/java/exceptionthrowingdialogueinterfaces/com/palantir/product/ComplexExample.java +++ b/conjure-java-core/src/integrationInput/java/exceptionthrowingdialogueinterfaces/com/palantir/product/ComplexExample.java @@ -140,6 +140,7 @@ public static Builder builder() { public static final class Builder { boolean _buildInvoked; + @JsonSetter(value = "metadata", nulls = Nulls.SKIP, contentNulls = Nulls.AS_EMPTY) private Map>> metadata = new LinkedHashMap<>(); private EnumExample status; @@ -159,7 +160,6 @@ public Builder from(ComplexExample other) { return this; } - @JsonSetter(value = "metadata", nulls = Nulls.SKIP, contentNulls = Nulls.AS_EMPTY) public Builder metadata(@Nonnull Map>> metadata) { checkNotBuilt(); this.metadata = new LinkedHashMap<>(Preconditions.checkNotNull(metadata, "metadata cannot be null")); diff --git a/conjure-java-core/src/integrationInput/java/exceptionthrowingdialogueinterfaces/com/palantir/product/EmptyUnion.java b/conjure-java-core/src/integrationInput/java/exceptionthrowingdialogueinterfaces/com/palantir/product/EmptyUnion.java index 0da7d0aa5..5c860aa83 100644 --- a/conjure-java-core/src/integrationInput/java/exceptionthrowingdialogueinterfaces/com/palantir/product/EmptyUnion.java +++ b/conjure-java-core/src/integrationInput/java/exceptionthrowingdialogueinterfaces/com/palantir/product/EmptyUnion.java @@ -3,14 +3,23 @@ import com.fasterxml.jackson.annotation.JsonAnyGetter; import com.fasterxml.jackson.annotation.JsonAnySetter; import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.annotation.JsonTypeInfo; import com.fasterxml.jackson.annotation.JsonValue; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.JsonToken; +import com.fasterxml.jackson.core.util.JsonParserSequence; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonDeserializer; +import com.fasterxml.jackson.databind.JsonMappingException; +import com.fasterxml.jackson.databind.MapperFeature; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.deser.ResolvableDeserializer; +import com.fasterxml.jackson.databind.util.TokenBuffer; import com.palantir.logsafe.Preconditions; import com.palantir.logsafe.Safe; import com.palantir.logsafe.SafeArg; import com.palantir.logsafe.exceptions.SafeIllegalArgumentException; +import java.io.IOException; import java.util.Collections; import java.util.HashMap; import java.util.Map; @@ -20,6 +29,7 @@ import javax.annotation.processing.Generated; @Generated("com.palantir.conjure.java.types.UnionGenerator") +@JsonDeserialize(using = EmptyUnion.Deserializer.class) public final class EmptyUnion { private final Base value; @@ -113,13 +123,6 @@ public interface Completed_StageVisitorBuilder { Visitor build(); } - @JsonTypeInfo( - use = JsonTypeInfo.Id.NAME, - include = JsonTypeInfo.As.EXISTING_PROPERTY, - property = "type", - visible = true, - defaultImpl = UnknownWrapper.class) - @JsonIgnoreProperties(ignoreUnknown = true) private interface Base { T accept(Visitor visitor); } @@ -183,4 +186,124 @@ public String toString() { return "UnknownWrapper{type: " + type + ", value: " + value + '}'; } } + + static final class Deserializer extends JsonDeserializer implements ResolvableDeserializer { + private static final Class[] VARIANT_TYPES = new Class[] {}; + + private volatile JsonDeserializer[] deserializers; + + @Override + public void resolve(DeserializationContext context) throws JsonMappingException { + deserializers = new JsonDeserializer[VARIANT_TYPES.length]; + } + + @Override + public boolean isCachable() { + return true; + } + + @Override + public EmptyUnion deserialize(JsonParser parser, DeserializationContext context) throws IOException { + if (!parser.isExpectedStartObjectToken()) { + return context.reportInputMismatch( + EmptyUnion.class, "Expected a JSON object for union deserialization"); + } + JsonToken firstToken = parser.nextToken(); + if (firstToken == JsonToken.FIELD_NAME && isTypeField(parser.currentName(), context)) { + if (parser.nextToken() != JsonToken.VALUE_STRING) { + return context.reportInputMismatch(EmptyUnion.class, "Union discriminator 'type' must be a string"); + } + String type = parser.getText(); + parser.nextToken(); + return deserializeSelected(parser, context, type); + } + return deserializeBuffered(parser, context); + } + + private EmptyUnion deserializeBuffered(JsonParser parser, DeserializationContext context) throws IOException { + try (TokenBuffer buffer = context.bufferForInputBuffering(parser)) { + buffer.writeStartObject(); + JsonToken token = parser.currentToken(); + while (token == JsonToken.FIELD_NAME) { + String fieldName = parser.currentName(); + JsonToken valueToken = parser.nextToken(); + if (isTypeField(fieldName, context)) { + if (valueToken != JsonToken.VALUE_STRING) { + return context.reportInputMismatch( + EmptyUnion.class, "Union discriminator 'type' must be a string"); + } + String type = parser.getText(); + parser.nextToken(); + try (JsonParser bufferedParser = buffer.asParser(parser)) { + JsonParser combinedParser = + JsonParserSequence.createFlattened(true, bufferedParser, parser); + combinedParser.nextToken(); + return deserializeSelected(combinedParser, context, type); + } + } + buffer.writeFieldName(fieldName); + buffer.copyCurrentStructure(parser); + token = parser.nextToken(); + } + if (token != JsonToken.END_OBJECT) { + return context.reportInputMismatch( + EmptyUnion.class, "Expected the end of a JSON object while deserializing a union"); + } + } + return context.reportInputMismatch(EmptyUnion.class, "Union discriminator 'type' is required"); + } + + private static boolean isTypeField(String fieldName, DeserializationContext context) { + return "type".equals(fieldName) + || (context.isEnabled(MapperFeature.ACCEPT_CASE_INSENSITIVE_PROPERTIES) + && "type".equalsIgnoreCase(fieldName)); + } + + private EmptyUnion deserializeSelected(JsonParser parser, DeserializationContext context, String type) + throws IOException { + int variantIndex = + switch (type) { + default -> -1; + }; + if (variantIndex < 0) { + return deserializeUnknown(parser, context, type); + } + JsonDeserializer deserializer = deserializers[variantIndex]; + if (deserializer == null) { + deserializer = resolveDeserializer(context, variantIndex); + } + return new EmptyUnion((Base) deserializer.deserialize(parser, context)); + } + + private synchronized JsonDeserializer resolveDeserializer(DeserializationContext context, int variantIndex) + throws JsonMappingException { + JsonDeserializer deserializer = deserializers[variantIndex]; + if (deserializer == null) { + deserializer = context.findRootValueDeserializer(context.constructType(VARIANT_TYPES[variantIndex])); + JsonDeserializer[] updated = deserializers.clone(); + updated[variantIndex] = deserializer; + deserializers = updated; + } + return deserializer; + } + + private static EmptyUnion deserializeUnknown(JsonParser parser, DeserializationContext context, String type) + throws IOException { + Map values = new HashMap<>(); + if (parser.currentToken() == JsonToken.START_OBJECT) { + parser.nextToken(); + } + while (parser.currentToken() == JsonToken.FIELD_NAME) { + String fieldName = parser.currentName(); + parser.nextToken(); + values.put(fieldName, context.readValue(parser, Object.class)); + parser.nextToken(); + } + if (parser.currentToken() != JsonToken.END_OBJECT) { + return context.reportInputMismatch( + EmptyUnion.class, "Expected the end of a JSON object while deserializing a union"); + } + return new EmptyUnion(new UnknownWrapper(type, values)); + } + } } diff --git a/conjure-java-core/src/integrationInput/java/exceptionthrowingdialogueinterfaces/com/palantir/product/NestedCollectionExample.java b/conjure-java-core/src/integrationInput/java/exceptionthrowingdialogueinterfaces/com/palantir/product/NestedCollectionExample.java index 242b1df8e..688806d42 100644 --- a/conjure-java-core/src/integrationInput/java/exceptionthrowingdialogueinterfaces/com/palantir/product/NestedCollectionExample.java +++ b/conjure-java-core/src/integrationInput/java/exceptionthrowingdialogueinterfaces/com/palantir/product/NestedCollectionExample.java @@ -138,8 +138,10 @@ public static final class Builder { private List> nestedList = ConjureCollections.newList(); + @JsonSetter(value = "nestedMap", nulls = Nulls.SKIP) private Map> nestedMap = new LinkedHashMap<>(); + @JsonSetter(value = "mixedCollection", nulls = Nulls.SKIP) private Map> mixedCollection = new LinkedHashMap<>(); private Builder() {} @@ -173,7 +175,6 @@ public Builder nestedList(List nestedList) { return this; } - @JsonSetter(value = "nestedMap", nulls = Nulls.SKIP) public Builder nestedMap(@Nonnull Map> nestedMap) { checkNotBuilt(); this.nestedMap = new LinkedHashMap<>(Preconditions.checkNotNull(nestedMap, "nestedMap cannot be null")); @@ -192,7 +193,6 @@ public Builder nestedMap(String key, Map value) { return this; } - @JsonSetter(value = "mixedCollection", nulls = Nulls.SKIP) public Builder mixedCollection(@Nonnull Map> mixedCollection) { checkNotBuilt(); this.mixedCollection = diff --git a/conjure-java-core/src/integrationInput/java/exceptionthrowingdialogueinterfaces/com/palantir/product/SimpleUnion.java b/conjure-java-core/src/integrationInput/java/exceptionthrowingdialogueinterfaces/com/palantir/product/SimpleUnion.java index 7adec5ce9..25741e338 100644 --- a/conjure-java-core/src/integrationInput/java/exceptionthrowingdialogueinterfaces/com/palantir/product/SimpleUnion.java +++ b/conjure-java-core/src/integrationInput/java/exceptionthrowingdialogueinterfaces/com/palantir/product/SimpleUnion.java @@ -6,15 +6,24 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.annotation.JsonSetter; -import com.fasterxml.jackson.annotation.JsonSubTypes; -import com.fasterxml.jackson.annotation.JsonTypeInfo; import com.fasterxml.jackson.annotation.JsonTypeName; import com.fasterxml.jackson.annotation.JsonValue; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.JsonToken; +import com.fasterxml.jackson.core.util.JsonParserSequence; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonDeserializer; +import com.fasterxml.jackson.databind.JsonMappingException; +import com.fasterxml.jackson.databind.MapperFeature; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.deser.ResolvableDeserializer; +import com.fasterxml.jackson.databind.util.TokenBuffer; import com.palantir.conjure.java.lib.SafeLong; import com.palantir.logsafe.Preconditions; import com.palantir.logsafe.Safe; import com.palantir.logsafe.SafeArg; import com.palantir.logsafe.exceptions.SafeIllegalArgumentException; +import java.io.IOException; import java.util.Collections; import java.util.HashMap; import java.util.Map; @@ -25,6 +34,7 @@ import javax.annotation.processing.Generated; @Generated("com.palantir.conjure.java.types.UnionGenerator") +@JsonDeserialize(using = SimpleUnion.Deserializer.class) public final class SimpleUnion { private final Base value; @@ -206,23 +216,12 @@ public interface Completed_StageVisitorBuilder { Visitor build(); } - @JsonTypeInfo( - use = JsonTypeInfo.Id.NAME, - include = JsonTypeInfo.As.EXISTING_PROPERTY, - property = "type", - visible = true, - defaultImpl = UnknownWrapper.class) - @JsonSubTypes({ - @JsonSubTypes.Type(FooWrapper.class), - @JsonSubTypes.Type(BarWrapper.class), - @JsonSubTypes.Type(BazWrapper.class) - }) - @JsonIgnoreProperties(ignoreUnknown = true) private interface Base { T accept(Visitor visitor); } @JsonTypeName("foo") + @JsonIgnoreProperties(ignoreUnknown = true) private static final class FooWrapper implements Base { private final String value; @@ -268,6 +267,7 @@ public String toString() { } @JsonTypeName("bar") + @JsonIgnoreProperties(ignoreUnknown = true) private static final class BarWrapper implements Base { private final int value; @@ -313,6 +313,7 @@ public String toString() { } @JsonTypeName("baz") + @JsonIgnoreProperties(ignoreUnknown = true) private static final class BazWrapper implements Base { private final SafeLong value; @@ -416,4 +417,129 @@ public String toString() { return "UnknownWrapper{type: " + type + ", value: " + value + '}'; } } + + static final class Deserializer extends JsonDeserializer implements ResolvableDeserializer { + private static final Class[] VARIANT_TYPES = + new Class[] {FooWrapper.class, BarWrapper.class, BazWrapper.class}; + + private volatile JsonDeserializer[] deserializers; + + @Override + public void resolve(DeserializationContext context) throws JsonMappingException { + deserializers = new JsonDeserializer[VARIANT_TYPES.length]; + } + + @Override + public boolean isCachable() { + return true; + } + + @Override + public SimpleUnion deserialize(JsonParser parser, DeserializationContext context) throws IOException { + if (!parser.isExpectedStartObjectToken()) { + return context.reportInputMismatch( + SimpleUnion.class, "Expected a JSON object for union deserialization"); + } + JsonToken firstToken = parser.nextToken(); + if (firstToken == JsonToken.FIELD_NAME && isTypeField(parser.currentName(), context)) { + if (parser.nextToken() != JsonToken.VALUE_STRING) { + return context.reportInputMismatch( + SimpleUnion.class, "Union discriminator 'type' must be a string"); + } + String type = parser.getText(); + parser.nextToken(); + return deserializeSelected(parser, context, type); + } + return deserializeBuffered(parser, context); + } + + private SimpleUnion deserializeBuffered(JsonParser parser, DeserializationContext context) throws IOException { + try (TokenBuffer buffer = context.bufferForInputBuffering(parser)) { + buffer.writeStartObject(); + JsonToken token = parser.currentToken(); + while (token == JsonToken.FIELD_NAME) { + String fieldName = parser.currentName(); + JsonToken valueToken = parser.nextToken(); + if (isTypeField(fieldName, context)) { + if (valueToken != JsonToken.VALUE_STRING) { + return context.reportInputMismatch( + SimpleUnion.class, "Union discriminator 'type' must be a string"); + } + String type = parser.getText(); + parser.nextToken(); + try (JsonParser bufferedParser = buffer.asParser(parser)) { + JsonParser combinedParser = + JsonParserSequence.createFlattened(true, bufferedParser, parser); + combinedParser.nextToken(); + return deserializeSelected(combinedParser, context, type); + } + } + buffer.writeFieldName(fieldName); + buffer.copyCurrentStructure(parser); + token = parser.nextToken(); + } + if (token != JsonToken.END_OBJECT) { + return context.reportInputMismatch( + SimpleUnion.class, "Expected the end of a JSON object while deserializing a union"); + } + } + return context.reportInputMismatch(SimpleUnion.class, "Union discriminator 'type' is required"); + } + + private static boolean isTypeField(String fieldName, DeserializationContext context) { + return "type".equals(fieldName) + || (context.isEnabled(MapperFeature.ACCEPT_CASE_INSENSITIVE_PROPERTIES) + && "type".equalsIgnoreCase(fieldName)); + } + + private SimpleUnion deserializeSelected(JsonParser parser, DeserializationContext context, String type) + throws IOException { + int variantIndex = + switch (type) { + case "foo" -> 0; + case "bar" -> 1; + case "baz" -> 2; + default -> -1; + }; + if (variantIndex < 0) { + return deserializeUnknown(parser, context, type); + } + JsonDeserializer deserializer = deserializers[variantIndex]; + if (deserializer == null) { + deserializer = resolveDeserializer(context, variantIndex); + } + return new SimpleUnion((Base) deserializer.deserialize(parser, context)); + } + + private synchronized JsonDeserializer resolveDeserializer(DeserializationContext context, int variantIndex) + throws JsonMappingException { + JsonDeserializer deserializer = deserializers[variantIndex]; + if (deserializer == null) { + deserializer = context.findRootValueDeserializer(context.constructType(VARIANT_TYPES[variantIndex])); + JsonDeserializer[] updated = deserializers.clone(); + updated[variantIndex] = deserializer; + deserializers = updated; + } + return deserializer; + } + + private static SimpleUnion deserializeUnknown(JsonParser parser, DeserializationContext context, String type) + throws IOException { + Map values = new HashMap<>(); + if (parser.currentToken() == JsonToken.START_OBJECT) { + parser.nextToken(); + } + while (parser.currentToken() == JsonToken.FIELD_NAME) { + String fieldName = parser.currentName(); + parser.nextToken(); + values.put(fieldName, context.readValue(parser, Object.class)); + parser.nextToken(); + } + if (parser.currentToken() != JsonToken.END_OBJECT) { + return context.reportInputMismatch( + SimpleUnion.class, "Expected the end of a JSON object while deserializing a union"); + } + return new SimpleUnion(new UnknownWrapper(type, values)); + } + } } diff --git a/conjure-java-core/src/integrationInput/java/exceptionthrowingdialogueinterfaces/com/palantir/product/UnionExample.java b/conjure-java-core/src/integrationInput/java/exceptionthrowingdialogueinterfaces/com/palantir/product/UnionExample.java index 8575892d4..b508d12f2 100644 --- a/conjure-java-core/src/integrationInput/java/exceptionthrowingdialogueinterfaces/com/palantir/product/UnionExample.java +++ b/conjure-java-core/src/integrationInput/java/exceptionthrowingdialogueinterfaces/com/palantir/product/UnionExample.java @@ -6,15 +6,24 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.annotation.JsonSetter; -import com.fasterxml.jackson.annotation.JsonSubTypes; -import com.fasterxml.jackson.annotation.JsonTypeInfo; import com.fasterxml.jackson.annotation.JsonTypeName; import com.fasterxml.jackson.annotation.JsonValue; import com.fasterxml.jackson.annotation.Nulls; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.JsonToken; +import com.fasterxml.jackson.core.util.JsonParserSequence; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonDeserializer; +import com.fasterxml.jackson.databind.JsonMappingException; +import com.fasterxml.jackson.databind.MapperFeature; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.deser.ResolvableDeserializer; +import com.fasterxml.jackson.databind.util.TokenBuffer; import com.palantir.logsafe.Preconditions; import com.palantir.logsafe.Safe; import com.palantir.logsafe.SafeArg; import com.palantir.logsafe.exceptions.SafeIllegalArgumentException; +import java.io.IOException; import java.util.Collections; import java.util.HashMap; import java.util.List; @@ -27,6 +36,7 @@ import javax.annotation.processing.Generated; @Generated("com.palantir.conjure.java.types.UnionGenerator") +@JsonDeserialize(using = UnionExample.Deserializer.class) public final class UnionExample { private final Base value; @@ -270,25 +280,12 @@ public interface Completed_StageVisitorBuilder { Visitor build(); } - @JsonTypeInfo( - use = JsonTypeInfo.Id.NAME, - include = JsonTypeInfo.As.EXISTING_PROPERTY, - property = "type", - visible = true, - defaultImpl = UnknownWrapper.class) - @JsonSubTypes({ - @JsonSubTypes.Type(StringVariantWrapper.class), - @JsonSubTypes.Type(IntVariantWrapper.class), - @JsonSubTypes.Type(ObjectVariantWrapper.class), - @JsonSubTypes.Type(CollectionVariantWrapper.class), - @JsonSubTypes.Type(OptionalVariantWrapper.class) - }) - @JsonIgnoreProperties(ignoreUnknown = true) private interface Base { T accept(Visitor visitor); } @JsonTypeName("stringVariant") + @JsonIgnoreProperties(ignoreUnknown = true) private static final class StringVariantWrapper implements Base { private final String value; @@ -334,6 +331,7 @@ public String toString() { } @JsonTypeName("intVariant") + @JsonIgnoreProperties(ignoreUnknown = true) private static final class IntVariantWrapper implements Base { private final int value; @@ -379,6 +377,7 @@ public String toString() { } @JsonTypeName("objectVariant") + @JsonIgnoreProperties(ignoreUnknown = true) private static final class ObjectVariantWrapper implements Base { private final ObjectReference value; @@ -424,6 +423,7 @@ public String toString() { } @JsonTypeName("collectionVariant") + @JsonIgnoreProperties(ignoreUnknown = true) private static final class CollectionVariantWrapper implements Base { private final List value; @@ -471,6 +471,7 @@ public String toString() { } @JsonTypeName("optionalVariant") + @JsonIgnoreProperties(ignoreUnknown = true) private static final class OptionalVariantWrapper implements Base { private final Optional value; @@ -576,4 +577,136 @@ public String toString() { return "UnknownWrapper{type: " + type + ", value: " + value + '}'; } } + + static final class Deserializer extends JsonDeserializer implements ResolvableDeserializer { + private static final Class[] VARIANT_TYPES = new Class[] { + StringVariantWrapper.class, + IntVariantWrapper.class, + ObjectVariantWrapper.class, + CollectionVariantWrapper.class, + OptionalVariantWrapper.class + }; + + private volatile JsonDeserializer[] deserializers; + + @Override + public void resolve(DeserializationContext context) throws JsonMappingException { + deserializers = new JsonDeserializer[VARIANT_TYPES.length]; + } + + @Override + public boolean isCachable() { + return true; + } + + @Override + public UnionExample deserialize(JsonParser parser, DeserializationContext context) throws IOException { + if (!parser.isExpectedStartObjectToken()) { + return context.reportInputMismatch( + UnionExample.class, "Expected a JSON object for union deserialization"); + } + JsonToken firstToken = parser.nextToken(); + if (firstToken == JsonToken.FIELD_NAME && isTypeField(parser.currentName(), context)) { + if (parser.nextToken() != JsonToken.VALUE_STRING) { + return context.reportInputMismatch( + UnionExample.class, "Union discriminator 'type' must be a string"); + } + String type = parser.getText(); + parser.nextToken(); + return deserializeSelected(parser, context, type); + } + return deserializeBuffered(parser, context); + } + + private UnionExample deserializeBuffered(JsonParser parser, DeserializationContext context) throws IOException { + try (TokenBuffer buffer = context.bufferForInputBuffering(parser)) { + buffer.writeStartObject(); + JsonToken token = parser.currentToken(); + while (token == JsonToken.FIELD_NAME) { + String fieldName = parser.currentName(); + JsonToken valueToken = parser.nextToken(); + if (isTypeField(fieldName, context)) { + if (valueToken != JsonToken.VALUE_STRING) { + return context.reportInputMismatch( + UnionExample.class, "Union discriminator 'type' must be a string"); + } + String type = parser.getText(); + parser.nextToken(); + try (JsonParser bufferedParser = buffer.asParser(parser)) { + JsonParser combinedParser = + JsonParserSequence.createFlattened(true, bufferedParser, parser); + combinedParser.nextToken(); + return deserializeSelected(combinedParser, context, type); + } + } + buffer.writeFieldName(fieldName); + buffer.copyCurrentStructure(parser); + token = parser.nextToken(); + } + if (token != JsonToken.END_OBJECT) { + return context.reportInputMismatch( + UnionExample.class, "Expected the end of a JSON object while deserializing a union"); + } + } + return context.reportInputMismatch(UnionExample.class, "Union discriminator 'type' is required"); + } + + private static boolean isTypeField(String fieldName, DeserializationContext context) { + return "type".equals(fieldName) + || (context.isEnabled(MapperFeature.ACCEPT_CASE_INSENSITIVE_PROPERTIES) + && "type".equalsIgnoreCase(fieldName)); + } + + private UnionExample deserializeSelected(JsonParser parser, DeserializationContext context, String type) + throws IOException { + int variantIndex = + switch (type) { + case "stringVariant" -> 0; + case "intVariant" -> 1; + case "objectVariant" -> 2; + case "collectionVariant" -> 3; + case "optionalVariant" -> 4; + default -> -1; + }; + if (variantIndex < 0) { + return deserializeUnknown(parser, context, type); + } + JsonDeserializer deserializer = deserializers[variantIndex]; + if (deserializer == null) { + deserializer = resolveDeserializer(context, variantIndex); + } + return new UnionExample((Base) deserializer.deserialize(parser, context)); + } + + private synchronized JsonDeserializer resolveDeserializer(DeserializationContext context, int variantIndex) + throws JsonMappingException { + JsonDeserializer deserializer = deserializers[variantIndex]; + if (deserializer == null) { + deserializer = context.findRootValueDeserializer(context.constructType(VARIANT_TYPES[variantIndex])); + JsonDeserializer[] updated = deserializers.clone(); + updated[variantIndex] = deserializer; + deserializers = updated; + } + return deserializer; + } + + private static UnionExample deserializeUnknown(JsonParser parser, DeserializationContext context, String type) + throws IOException { + Map values = new HashMap<>(); + if (parser.currentToken() == JsonToken.START_OBJECT) { + parser.nextToken(); + } + while (parser.currentToken() == JsonToken.FIELD_NAME) { + String fieldName = parser.currentName(); + parser.nextToken(); + values.put(fieldName, context.readValue(parser, Object.class)); + parser.nextToken(); + } + if (parser.currentToken() != JsonToken.END_OBJECT) { + return context.reportInputMismatch( + UnionExample.class, "Expected the end of a JSON object while deserializing a union"); + } + return new UnionExample(new UnknownWrapper(type, values)); + } + } } diff --git a/conjure-java-core/src/integrationInput/java/excludeasyncinterfaces/com/palantir/product/datasets/BackingFileSystem.java b/conjure-java-core/src/integrationInput/java/excludeasyncinterfaces/com/palantir/product/datasets/BackingFileSystem.java index cf128f388..4fa981a53 100644 --- a/conjure-java-core/src/integrationInput/java/excludeasyncinterfaces/com/palantir/product/datasets/BackingFileSystem.java +++ b/conjure-java-core/src/integrationInput/java/excludeasyncinterfaces/com/palantir/product/datasets/BackingFileSystem.java @@ -131,6 +131,7 @@ public static final class Builder { private String baseUri; + @JsonSetter(value = "configuration", nulls = Nulls.SKIP) private Map configuration = new LinkedHashMap<>(); private Builder() {} @@ -158,7 +159,6 @@ public Builder baseUri(@Nonnull String baseUri) { return this; } - @JsonSetter(value = "configuration", nulls = Nulls.SKIP) public Builder configuration(@Nonnull Map configuration) { checkNotBuilt(); this.configuration = diff --git a/conjure-java-core/src/integrationInput/java/excludeemptycollections/com/palantir/product/CollectionsTestObject.java b/conjure-java-core/src/integrationInput/java/excludeemptycollections/com/palantir/product/CollectionsTestObject.java index 0c2ae2774..f03c2c7fb 100644 --- a/conjure-java-core/src/integrationInput/java/excludeemptycollections/com/palantir/product/CollectionsTestObject.java +++ b/conjure-java-core/src/integrationInput/java/excludeemptycollections/com/palantir/product/CollectionsTestObject.java @@ -188,6 +188,7 @@ public static final class Builder { private List items = ConjureCollections.newList(); + @JsonSetter(value = "itemsMap", nulls = Nulls.SKIP) private Map itemsMap = new LinkedHashMap<>(); private Optional optionalItem = Optional.empty(); @@ -233,7 +234,6 @@ public Builder items(String items) { return this; } - @JsonSetter(value = "itemsMap", nulls = Nulls.SKIP) public Builder itemsMap(@Nonnull Map itemsMap) { checkNotBuilt(); this.itemsMap = new LinkedHashMap<>(Preconditions.checkNotNull(itemsMap, "itemsMap cannot be null")); diff --git a/conjure-java-core/src/integrationInput/java/externalfallbacktypes/com/palantir/product/external/UnionWithExternal.java b/conjure-java-core/src/integrationInput/java/externalfallbacktypes/com/palantir/product/external/UnionWithExternal.java index 0028521e1..67807dd9e 100644 --- a/conjure-java-core/src/integrationInput/java/externalfallbacktypes/com/palantir/product/external/UnionWithExternal.java +++ b/conjure-java-core/src/integrationInput/java/externalfallbacktypes/com/palantir/product/external/UnionWithExternal.java @@ -6,14 +6,23 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.annotation.JsonSetter; -import com.fasterxml.jackson.annotation.JsonSubTypes; -import com.fasterxml.jackson.annotation.JsonTypeInfo; import com.fasterxml.jackson.annotation.JsonTypeName; import com.fasterxml.jackson.annotation.JsonValue; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.JsonToken; +import com.fasterxml.jackson.core.util.JsonParserSequence; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonDeserializer; +import com.fasterxml.jackson.databind.JsonMappingException; +import com.fasterxml.jackson.databind.MapperFeature; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.deser.ResolvableDeserializer; +import com.fasterxml.jackson.databind.util.TokenBuffer; import com.palantir.logsafe.Preconditions; import com.palantir.logsafe.Safe; import com.palantir.logsafe.SafeArg; import com.palantir.logsafe.exceptions.SafeIllegalArgumentException; +import java.io.IOException; import java.util.Collections; import java.util.HashMap; import java.util.Map; @@ -25,6 +34,7 @@ /** A type which can either be a StringExample, a set of strings, or an integer. */ @Generated("com.palantir.conjure.java.types.UnionGenerator") +@JsonDeserialize(using = UnionWithExternal.Deserializer.class) public final class UnionWithExternal { private final Base value; @@ -155,19 +165,12 @@ public interface Completed_StageVisitorBuilder { Visitor build(); } - @JsonTypeInfo( - use = JsonTypeInfo.Id.NAME, - include = JsonTypeInfo.As.EXISTING_PROPERTY, - property = "type", - visible = true, - defaultImpl = UnknownWrapper.class) - @JsonSubTypes(@JsonSubTypes.Type(FieldWrapper.class)) - @JsonIgnoreProperties(ignoreUnknown = true) private interface Base { T accept(Visitor visitor); } @JsonTypeName("field") + @JsonIgnoreProperties(ignoreUnknown = true) private static final class FieldWrapper implements Base { private final String value; @@ -271,4 +274,127 @@ public String toString() { return "UnknownWrapper{type: " + type + ", value: " + value + '}'; } } + + static final class Deserializer extends JsonDeserializer implements ResolvableDeserializer { + private static final Class[] VARIANT_TYPES = new Class[] {FieldWrapper.class}; + + private volatile JsonDeserializer[] deserializers; + + @Override + public void resolve(DeserializationContext context) throws JsonMappingException { + deserializers = new JsonDeserializer[VARIANT_TYPES.length]; + } + + @Override + public boolean isCachable() { + return true; + } + + @Override + public UnionWithExternal deserialize(JsonParser parser, DeserializationContext context) throws IOException { + if (!parser.isExpectedStartObjectToken()) { + return context.reportInputMismatch( + UnionWithExternal.class, "Expected a JSON object for union deserialization"); + } + JsonToken firstToken = parser.nextToken(); + if (firstToken == JsonToken.FIELD_NAME && isTypeField(parser.currentName(), context)) { + if (parser.nextToken() != JsonToken.VALUE_STRING) { + return context.reportInputMismatch( + UnionWithExternal.class, "Union discriminator 'type' must be a string"); + } + String type = parser.getText(); + parser.nextToken(); + return deserializeSelected(parser, context, type); + } + return deserializeBuffered(parser, context); + } + + private UnionWithExternal deserializeBuffered(JsonParser parser, DeserializationContext context) + throws IOException { + try (TokenBuffer buffer = context.bufferForInputBuffering(parser)) { + buffer.writeStartObject(); + JsonToken token = parser.currentToken(); + while (token == JsonToken.FIELD_NAME) { + String fieldName = parser.currentName(); + JsonToken valueToken = parser.nextToken(); + if (isTypeField(fieldName, context)) { + if (valueToken != JsonToken.VALUE_STRING) { + return context.reportInputMismatch( + UnionWithExternal.class, "Union discriminator 'type' must be a string"); + } + String type = parser.getText(); + parser.nextToken(); + try (JsonParser bufferedParser = buffer.asParser(parser)) { + JsonParser combinedParser = + JsonParserSequence.createFlattened(true, bufferedParser, parser); + combinedParser.nextToken(); + return deserializeSelected(combinedParser, context, type); + } + } + buffer.writeFieldName(fieldName); + buffer.copyCurrentStructure(parser); + token = parser.nextToken(); + } + if (token != JsonToken.END_OBJECT) { + return context.reportInputMismatch( + UnionWithExternal.class, "Expected the end of a JSON object while deserializing a union"); + } + } + return context.reportInputMismatch(UnionWithExternal.class, "Union discriminator 'type' is required"); + } + + private static boolean isTypeField(String fieldName, DeserializationContext context) { + return "type".equals(fieldName) + || (context.isEnabled(MapperFeature.ACCEPT_CASE_INSENSITIVE_PROPERTIES) + && "type".equalsIgnoreCase(fieldName)); + } + + private UnionWithExternal deserializeSelected(JsonParser parser, DeserializationContext context, String type) + throws IOException { + int variantIndex = + switch (type) { + case "field" -> 0; + default -> -1; + }; + if (variantIndex < 0) { + return deserializeUnknown(parser, context, type); + } + JsonDeserializer deserializer = deserializers[variantIndex]; + if (deserializer == null) { + deserializer = resolveDeserializer(context, variantIndex); + } + return new UnionWithExternal((Base) deserializer.deserialize(parser, context)); + } + + private synchronized JsonDeserializer resolveDeserializer(DeserializationContext context, int variantIndex) + throws JsonMappingException { + JsonDeserializer deserializer = deserializers[variantIndex]; + if (deserializer == null) { + deserializer = context.findRootValueDeserializer(context.constructType(VARIANT_TYPES[variantIndex])); + JsonDeserializer[] updated = deserializers.clone(); + updated[variantIndex] = deserializer; + deserializers = updated; + } + return deserializer; + } + + private static UnionWithExternal deserializeUnknown( + JsonParser parser, DeserializationContext context, String type) throws IOException { + Map values = new HashMap<>(); + if (parser.currentToken() == JsonToken.START_OBJECT) { + parser.nextToken(); + } + while (parser.currentToken() == JsonToken.FIELD_NAME) { + String fieldName = parser.currentName(); + parser.nextToken(); + values.put(fieldName, context.readValue(parser, Object.class)); + parser.nextToken(); + } + if (parser.currentToken() != JsonToken.END_OBJECT) { + return context.reportInputMismatch( + UnionWithExternal.class, "Expected the end of a JSON object while deserializing a union"); + } + return new UnionWithExternal(new UnknownWrapper(type, values)); + } + } } diff --git a/conjure-java-core/src/integrationInput/java/jakartaservice/com/palantir/product/datasets/BackingFileSystem.java b/conjure-java-core/src/integrationInput/java/jakartaservice/com/palantir/product/datasets/BackingFileSystem.java index e037d1118..ec19ae49d 100644 --- a/conjure-java-core/src/integrationInput/java/jakartaservice/com/palantir/product/datasets/BackingFileSystem.java +++ b/conjure-java-core/src/integrationInput/java/jakartaservice/com/palantir/product/datasets/BackingFileSystem.java @@ -131,6 +131,7 @@ public static final class Builder { private String baseUri; + @JsonSetter(value = "configuration", nulls = Nulls.SKIP) private Map configuration = new LinkedHashMap<>(); private Builder() {} @@ -158,7 +159,6 @@ public Builder baseUri(@Nonnull String baseUri) { return this; } - @JsonSetter(value = "configuration", nulls = Nulls.SKIP) public Builder configuration(@Nonnull Map configuration) { checkNotBuilt(); this.configuration = diff --git a/conjure-java-core/src/integrationInput/java/jersey/com/palantir/product/SimpleUnion.java b/conjure-java-core/src/integrationInput/java/jersey/com/palantir/product/SimpleUnion.java index c25dbc7de..e85d3b96a 100644 --- a/conjure-java-core/src/integrationInput/java/jersey/com/palantir/product/SimpleUnion.java +++ b/conjure-java-core/src/integrationInput/java/jersey/com/palantir/product/SimpleUnion.java @@ -6,15 +6,24 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.annotation.JsonSetter; -import com.fasterxml.jackson.annotation.JsonSubTypes; -import com.fasterxml.jackson.annotation.JsonTypeInfo; import com.fasterxml.jackson.annotation.JsonTypeName; import com.fasterxml.jackson.annotation.JsonValue; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.JsonToken; +import com.fasterxml.jackson.core.util.JsonParserSequence; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonDeserializer; +import com.fasterxml.jackson.databind.JsonMappingException; +import com.fasterxml.jackson.databind.MapperFeature; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.deser.ResolvableDeserializer; +import com.fasterxml.jackson.databind.util.TokenBuffer; import com.palantir.conjure.java.lib.SafeLong; import com.palantir.logsafe.Preconditions; import com.palantir.logsafe.Safe; import com.palantir.logsafe.SafeArg; import com.palantir.logsafe.exceptions.SafeIllegalArgumentException; +import java.io.IOException; import java.util.Collections; import java.util.HashMap; import java.util.Map; @@ -25,6 +34,7 @@ import javax.annotation.processing.Generated; @Generated("com.palantir.conjure.java.types.UnionGenerator") +@JsonDeserialize(using = SimpleUnion.Deserializer.class) public final class SimpleUnion { private final Base value; @@ -206,23 +216,12 @@ public interface Completed_StageVisitorBuilder { Visitor build(); } - @JsonTypeInfo( - use = JsonTypeInfo.Id.NAME, - include = JsonTypeInfo.As.EXISTING_PROPERTY, - property = "type", - visible = true, - defaultImpl = UnknownWrapper.class) - @JsonSubTypes({ - @JsonSubTypes.Type(FooWrapper.class), - @JsonSubTypes.Type(BarWrapper.class), - @JsonSubTypes.Type(BazWrapper.class) - }) - @JsonIgnoreProperties(ignoreUnknown = true) private interface Base { T accept(Visitor visitor); } @JsonTypeName("foo") + @JsonIgnoreProperties(ignoreUnknown = true) private static final class FooWrapper implements Base { private final String value; @@ -268,6 +267,7 @@ public String toString() { } @JsonTypeName("bar") + @JsonIgnoreProperties(ignoreUnknown = true) private static final class BarWrapper implements Base { private final int value; @@ -313,6 +313,7 @@ public String toString() { } @JsonTypeName("baz") + @JsonIgnoreProperties(ignoreUnknown = true) private static final class BazWrapper implements Base { private final SafeLong value; @@ -416,4 +417,129 @@ public String toString() { return "UnknownWrapper{type: " + type + ", value: " + value + '}'; } } + + static final class Deserializer extends JsonDeserializer implements ResolvableDeserializer { + private static final Class[] VARIANT_TYPES = + new Class[] {FooWrapper.class, BarWrapper.class, BazWrapper.class}; + + private volatile JsonDeserializer[] deserializers; + + @Override + public void resolve(DeserializationContext context) throws JsonMappingException { + deserializers = new JsonDeserializer[VARIANT_TYPES.length]; + } + + @Override + public boolean isCachable() { + return true; + } + + @Override + public SimpleUnion deserialize(JsonParser parser, DeserializationContext context) throws IOException { + if (!parser.isExpectedStartObjectToken()) { + return context.reportInputMismatch( + SimpleUnion.class, "Expected a JSON object for union deserialization"); + } + JsonToken firstToken = parser.nextToken(); + if (firstToken == JsonToken.FIELD_NAME && isTypeField(parser.currentName(), context)) { + if (parser.nextToken() != JsonToken.VALUE_STRING) { + return context.reportInputMismatch( + SimpleUnion.class, "Union discriminator 'type' must be a string"); + } + String type = parser.getText(); + parser.nextToken(); + return deserializeSelected(parser, context, type); + } + return deserializeBuffered(parser, context); + } + + private SimpleUnion deserializeBuffered(JsonParser parser, DeserializationContext context) throws IOException { + try (TokenBuffer buffer = context.bufferForInputBuffering(parser)) { + buffer.writeStartObject(); + JsonToken token = parser.currentToken(); + while (token == JsonToken.FIELD_NAME) { + String fieldName = parser.currentName(); + JsonToken valueToken = parser.nextToken(); + if (isTypeField(fieldName, context)) { + if (valueToken != JsonToken.VALUE_STRING) { + return context.reportInputMismatch( + SimpleUnion.class, "Union discriminator 'type' must be a string"); + } + String type = parser.getText(); + parser.nextToken(); + try (JsonParser bufferedParser = buffer.asParser(parser)) { + JsonParser combinedParser = + JsonParserSequence.createFlattened(true, bufferedParser, parser); + combinedParser.nextToken(); + return deserializeSelected(combinedParser, context, type); + } + } + buffer.writeFieldName(fieldName); + buffer.copyCurrentStructure(parser); + token = parser.nextToken(); + } + if (token != JsonToken.END_OBJECT) { + return context.reportInputMismatch( + SimpleUnion.class, "Expected the end of a JSON object while deserializing a union"); + } + } + return context.reportInputMismatch(SimpleUnion.class, "Union discriminator 'type' is required"); + } + + private static boolean isTypeField(String fieldName, DeserializationContext context) { + return "type".equals(fieldName) + || (context.isEnabled(MapperFeature.ACCEPT_CASE_INSENSITIVE_PROPERTIES) + && "type".equalsIgnoreCase(fieldName)); + } + + private SimpleUnion deserializeSelected(JsonParser parser, DeserializationContext context, String type) + throws IOException { + int variantIndex = + switch (type) { + case "foo" -> 0; + case "bar" -> 1; + case "baz" -> 2; + default -> -1; + }; + if (variantIndex < 0) { + return deserializeUnknown(parser, context, type); + } + JsonDeserializer deserializer = deserializers[variantIndex]; + if (deserializer == null) { + deserializer = resolveDeserializer(context, variantIndex); + } + return new SimpleUnion((Base) deserializer.deserialize(parser, context)); + } + + private synchronized JsonDeserializer resolveDeserializer(DeserializationContext context, int variantIndex) + throws JsonMappingException { + JsonDeserializer deserializer = deserializers[variantIndex]; + if (deserializer == null) { + deserializer = context.findRootValueDeserializer(context.constructType(VARIANT_TYPES[variantIndex])); + JsonDeserializer[] updated = deserializers.clone(); + updated[variantIndex] = deserializer; + deserializers = updated; + } + return deserializer; + } + + private static SimpleUnion deserializeUnknown(JsonParser parser, DeserializationContext context, String type) + throws IOException { + Map values = new HashMap<>(); + if (parser.currentToken() == JsonToken.START_OBJECT) { + parser.nextToken(); + } + while (parser.currentToken() == JsonToken.FIELD_NAME) { + String fieldName = parser.currentName(); + parser.nextToken(); + values.put(fieldName, context.readValue(parser, Object.class)); + parser.nextToken(); + } + if (parser.currentToken() != JsonToken.END_OBJECT) { + return context.reportInputMismatch( + SimpleUnion.class, "Expected the end of a JSON object while deserializing a union"); + } + return new SimpleUnion(new UnknownWrapper(type, values)); + } + } } diff --git a/conjure-java-core/src/integrationInput/java/jersey/com/palantir/product/datasets/BackingFileSystem.java b/conjure-java-core/src/integrationInput/java/jersey/com/palantir/product/datasets/BackingFileSystem.java index 87b1ce56c..6c4dedbc0 100644 --- a/conjure-java-core/src/integrationInput/java/jersey/com/palantir/product/datasets/BackingFileSystem.java +++ b/conjure-java-core/src/integrationInput/java/jersey/com/palantir/product/datasets/BackingFileSystem.java @@ -131,6 +131,7 @@ public static final class Builder { private String baseUri; + @JsonSetter(value = "configuration", nulls = Nulls.SKIP) private Map configuration = new LinkedHashMap<>(); private Builder() {} @@ -158,7 +159,6 @@ public Builder baseUri(@Nonnull String baseUri) { return this; } - @JsonSetter(value = "configuration", nulls = Nulls.SKIP) public Builder configuration(@Nonnull Map configuration) { checkNotBuilt(); this.configuration = diff --git a/conjure-java-core/src/integrationInput/java/sealedunions/com/palantir/product/CamelCaseUnion.java b/conjure-java-core/src/integrationInput/java/sealedunions/com/palantir/product/CamelCaseUnion.java index 5189f8585..1b2a1d942 100644 --- a/conjure-java-core/src/integrationInput/java/sealedunions/com/palantir/product/CamelCaseUnion.java +++ b/conjure-java-core/src/integrationInput/java/sealedunions/com/palantir/product/CamelCaseUnion.java @@ -6,13 +6,26 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.annotation.JsonSetter; -import com.fasterxml.jackson.annotation.JsonSubTypes; -import com.fasterxml.jackson.annotation.JsonTypeInfo; import com.fasterxml.jackson.annotation.JsonTypeName; +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.JsonToken; +import com.fasterxml.jackson.core.util.JsonParserSequence; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonDeserializer; +import com.fasterxml.jackson.databind.JsonMappingException; +import com.fasterxml.jackson.databind.JsonSerializer; +import com.fasterxml.jackson.databind.MapperFeature; +import com.fasterxml.jackson.databind.SerializerProvider; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import com.fasterxml.jackson.databind.deser.ResolvableDeserializer; +import com.fasterxml.jackson.databind.util.TokenBuffer; import com.palantir.logsafe.Preconditions; import com.palantir.logsafe.Safe; import com.palantir.logsafe.SafeArg; import com.palantir.logsafe.exceptions.SafeIllegalArgumentException; +import java.io.IOException; import java.util.Collections; import java.util.HashMap; import java.util.Map; @@ -23,13 +36,8 @@ import javax.annotation.processing.Generated; @Generated("com.palantir.conjure.java.types.UnionGenerator") -@JsonTypeInfo( - use = JsonTypeInfo.Id.NAME, - include = JsonTypeInfo.As.EXISTING_PROPERTY, - property = "type", - visible = true, - defaultImpl = CamelCaseUnion.Unknown.class) -@JsonSubTypes(@JsonSubTypes.Type(value = CamelCaseUnion.CamelCasedField.class, name = "camelCasedField")) +@JsonDeserialize(using = CamelCaseUnion.Deserializer.class) +@JsonSerialize(using = CamelCaseUnion.Serializer.class) @JsonIgnoreProperties(ignoreUnknown = true) public abstract sealed class CamelCaseUnion permits CamelCaseUnion.CamelCasedField, CamelCaseUnion.Unknown { public static CamelCaseUnion camelCasedField(String value) { @@ -61,6 +69,9 @@ public Known throwOnUnknown() { public sealed interface Known permits CamelCasedField {} @JsonTypeName("camelCasedField") + @JsonIgnoreProperties(ignoreUnknown = true) + @JsonDeserialize + @JsonSerialize public static final class CamelCasedField extends CamelCaseUnion implements Known { private final String value; @@ -105,6 +116,8 @@ public String toString() { } } + @JsonDeserialize + @JsonSerialize public static final class Unknown extends CamelCaseUnion { private final String type; @@ -165,6 +178,137 @@ public String toString() { } } + static final class Serializer extends JsonSerializer { + @Override + public void serialize(CamelCaseUnion value, JsonGenerator generator, SerializerProvider serializers) + throws IOException { + serializers.findValueSerializer(value.getClass()).serialize(value, generator, serializers); + } + } + + static final class Deserializer extends JsonDeserializer implements ResolvableDeserializer { + private static final Class[] VARIANT_TYPES = new Class[] {CamelCasedField.class}; + + private volatile JsonDeserializer[] deserializers; + + @Override + public void resolve(DeserializationContext context) throws JsonMappingException { + deserializers = new JsonDeserializer[VARIANT_TYPES.length]; + } + + @Override + public boolean isCachable() { + return true; + } + + @Override + public CamelCaseUnion deserialize(JsonParser parser, DeserializationContext context) throws IOException { + if (!parser.isExpectedStartObjectToken()) { + return context.reportInputMismatch( + CamelCaseUnion.class, "Expected a JSON object for union deserialization"); + } + JsonToken firstToken = parser.nextToken(); + if (firstToken == JsonToken.FIELD_NAME && isTypeField(parser.currentName(), context)) { + if (parser.nextToken() != JsonToken.VALUE_STRING) { + return context.reportInputMismatch( + CamelCaseUnion.class, "Union discriminator 'type' must be a string"); + } + String type = parser.getText(); + parser.nextToken(); + return deserializeSelected(parser, context, type); + } + return deserializeBuffered(parser, context); + } + + private CamelCaseUnion deserializeBuffered(JsonParser parser, DeserializationContext context) + throws IOException { + try (TokenBuffer buffer = context.bufferForInputBuffering(parser)) { + buffer.writeStartObject(); + JsonToken token = parser.currentToken(); + while (token == JsonToken.FIELD_NAME) { + String fieldName = parser.currentName(); + JsonToken valueToken = parser.nextToken(); + if (isTypeField(fieldName, context)) { + if (valueToken != JsonToken.VALUE_STRING) { + return context.reportInputMismatch( + CamelCaseUnion.class, "Union discriminator 'type' must be a string"); + } + String type = parser.getText(); + parser.nextToken(); + try (JsonParser bufferedParser = buffer.asParser(parser)) { + JsonParser combinedParser = + JsonParserSequence.createFlattened(true, bufferedParser, parser); + combinedParser.nextToken(); + return deserializeSelected(combinedParser, context, type); + } + } + buffer.writeFieldName(fieldName); + buffer.copyCurrentStructure(parser); + token = parser.nextToken(); + } + if (token != JsonToken.END_OBJECT) { + return context.reportInputMismatch( + CamelCaseUnion.class, "Expected the end of a JSON object while deserializing a union"); + } + } + return context.reportInputMismatch(CamelCaseUnion.class, "Union discriminator 'type' is required"); + } + + private static boolean isTypeField(String fieldName, DeserializationContext context) { + return "type".equals(fieldName) + || (context.isEnabled(MapperFeature.ACCEPT_CASE_INSENSITIVE_PROPERTIES) + && "type".equalsIgnoreCase(fieldName)); + } + + private CamelCaseUnion deserializeSelected(JsonParser parser, DeserializationContext context, String type) + throws IOException { + int variantIndex = + switch (type) { + case "camelCasedField" -> 0; + default -> -1; + }; + if (variantIndex < 0) { + return deserializeUnknown(parser, context, type); + } + JsonDeserializer deserializer = deserializers[variantIndex]; + if (deserializer == null) { + deserializer = resolveDeserializer(context, variantIndex); + } + return (CamelCaseUnion) deserializer.deserialize(parser, context); + } + + private synchronized JsonDeserializer resolveDeserializer(DeserializationContext context, int variantIndex) + throws JsonMappingException { + JsonDeserializer deserializer = deserializers[variantIndex]; + if (deserializer == null) { + deserializer = context.findRootValueDeserializer(context.constructType(VARIANT_TYPES[variantIndex])); + JsonDeserializer[] updated = deserializers.clone(); + updated[variantIndex] = deserializer; + deserializers = updated; + } + return deserializer; + } + + private static CamelCaseUnion deserializeUnknown(JsonParser parser, DeserializationContext context, String type) + throws IOException { + Map values = new HashMap<>(); + if (parser.currentToken() == JsonToken.START_OBJECT) { + parser.nextToken(); + } + while (parser.currentToken() == JsonToken.FIELD_NAME) { + String fieldName = parser.currentName(); + parser.nextToken(); + values.put(fieldName, context.readValue(parser, Object.class)); + parser.nextToken(); + } + if (parser.currentToken() != JsonToken.END_OBJECT) { + return context.reportInputMismatch( + CamelCaseUnion.class, "Expected the end of a JSON object while deserializing a union"); + } + return new Unknown(type, values); + } + } + public interface Visitor { T visitCamelCasedField(String value); diff --git a/conjure-java-core/src/integrationInput/java/sealedunions/com/palantir/product/EmptyUnion.java b/conjure-java-core/src/integrationInput/java/sealedunions/com/palantir/product/EmptyUnion.java index 8fc7dc5d0..ae28faf6a 100644 --- a/conjure-java-core/src/integrationInput/java/sealedunions/com/palantir/product/EmptyUnion.java +++ b/conjure-java-core/src/integrationInput/java/sealedunions/com/palantir/product/EmptyUnion.java @@ -5,12 +5,25 @@ import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.annotation.JsonSubTypes; -import com.fasterxml.jackson.annotation.JsonTypeInfo; +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.JsonToken; +import com.fasterxml.jackson.core.util.JsonParserSequence; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonDeserializer; +import com.fasterxml.jackson.databind.JsonMappingException; +import com.fasterxml.jackson.databind.JsonSerializer; +import com.fasterxml.jackson.databind.MapperFeature; +import com.fasterxml.jackson.databind.SerializerProvider; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import com.fasterxml.jackson.databind.deser.ResolvableDeserializer; +import com.fasterxml.jackson.databind.util.TokenBuffer; import com.palantir.logsafe.Preconditions; import com.palantir.logsafe.Safe; import com.palantir.logsafe.SafeArg; import com.palantir.logsafe.exceptions.SafeIllegalArgumentException; +import java.io.IOException; import java.util.Collections; import java.util.HashMap; import java.util.Map; @@ -21,13 +34,8 @@ import javax.annotation.processing.Generated; @Generated("com.palantir.conjure.java.types.UnionGenerator") -@JsonTypeInfo( - use = JsonTypeInfo.Id.NAME, - include = JsonTypeInfo.As.EXISTING_PROPERTY, - property = "type", - visible = true, - defaultImpl = EmptyUnion.Unknown.class) -@JsonSubTypes({}) +@JsonDeserialize(using = EmptyUnion.Deserializer.class) +@JsonSerialize(using = EmptyUnion.Serializer.class) @JsonIgnoreProperties(ignoreUnknown = true) public abstract sealed class EmptyUnion permits EmptyUnion.Unknown { public static EmptyUnion unknown(@Safe String type, Object value) { @@ -39,6 +47,8 @@ public static EmptyUnion unknown(@Safe String type, Object value) { public abstract T accept(Visitor visitor); + @JsonDeserialize + @JsonSerialize public static final class Unknown extends EmptyUnion { private final String type; @@ -99,6 +109,134 @@ public String toString() { } } + static final class Serializer extends JsonSerializer { + @Override + public void serialize(EmptyUnion value, JsonGenerator generator, SerializerProvider serializers) + throws IOException { + serializers.findValueSerializer(value.getClass()).serialize(value, generator, serializers); + } + } + + static final class Deserializer extends JsonDeserializer implements ResolvableDeserializer { + private static final Class[] VARIANT_TYPES = new Class[] {}; + + private volatile JsonDeserializer[] deserializers; + + @Override + public void resolve(DeserializationContext context) throws JsonMappingException { + deserializers = new JsonDeserializer[VARIANT_TYPES.length]; + } + + @Override + public boolean isCachable() { + return true; + } + + @Override + public EmptyUnion deserialize(JsonParser parser, DeserializationContext context) throws IOException { + if (!parser.isExpectedStartObjectToken()) { + return context.reportInputMismatch( + EmptyUnion.class, "Expected a JSON object for union deserialization"); + } + JsonToken firstToken = parser.nextToken(); + if (firstToken == JsonToken.FIELD_NAME && isTypeField(parser.currentName(), context)) { + if (parser.nextToken() != JsonToken.VALUE_STRING) { + return context.reportInputMismatch(EmptyUnion.class, "Union discriminator 'type' must be a string"); + } + String type = parser.getText(); + parser.nextToken(); + return deserializeSelected(parser, context, type); + } + return deserializeBuffered(parser, context); + } + + private EmptyUnion deserializeBuffered(JsonParser parser, DeserializationContext context) throws IOException { + try (TokenBuffer buffer = context.bufferForInputBuffering(parser)) { + buffer.writeStartObject(); + JsonToken token = parser.currentToken(); + while (token == JsonToken.FIELD_NAME) { + String fieldName = parser.currentName(); + JsonToken valueToken = parser.nextToken(); + if (isTypeField(fieldName, context)) { + if (valueToken != JsonToken.VALUE_STRING) { + return context.reportInputMismatch( + EmptyUnion.class, "Union discriminator 'type' must be a string"); + } + String type = parser.getText(); + parser.nextToken(); + try (JsonParser bufferedParser = buffer.asParser(parser)) { + JsonParser combinedParser = + JsonParserSequence.createFlattened(true, bufferedParser, parser); + combinedParser.nextToken(); + return deserializeSelected(combinedParser, context, type); + } + } + buffer.writeFieldName(fieldName); + buffer.copyCurrentStructure(parser); + token = parser.nextToken(); + } + if (token != JsonToken.END_OBJECT) { + return context.reportInputMismatch( + EmptyUnion.class, "Expected the end of a JSON object while deserializing a union"); + } + } + return context.reportInputMismatch(EmptyUnion.class, "Union discriminator 'type' is required"); + } + + private static boolean isTypeField(String fieldName, DeserializationContext context) { + return "type".equals(fieldName) + || (context.isEnabled(MapperFeature.ACCEPT_CASE_INSENSITIVE_PROPERTIES) + && "type".equalsIgnoreCase(fieldName)); + } + + private EmptyUnion deserializeSelected(JsonParser parser, DeserializationContext context, String type) + throws IOException { + int variantIndex = + switch (type) { + default -> -1; + }; + if (variantIndex < 0) { + return deserializeUnknown(parser, context, type); + } + JsonDeserializer deserializer = deserializers[variantIndex]; + if (deserializer == null) { + deserializer = resolveDeserializer(context, variantIndex); + } + return (EmptyUnion) deserializer.deserialize(parser, context); + } + + private synchronized JsonDeserializer resolveDeserializer(DeserializationContext context, int variantIndex) + throws JsonMappingException { + JsonDeserializer deserializer = deserializers[variantIndex]; + if (deserializer == null) { + deserializer = context.findRootValueDeserializer(context.constructType(VARIANT_TYPES[variantIndex])); + JsonDeserializer[] updated = deserializers.clone(); + updated[variantIndex] = deserializer; + deserializers = updated; + } + return deserializer; + } + + private static EmptyUnion deserializeUnknown(JsonParser parser, DeserializationContext context, String type) + throws IOException { + Map values = new HashMap<>(); + if (parser.currentToken() == JsonToken.START_OBJECT) { + parser.nextToken(); + } + while (parser.currentToken() == JsonToken.FIELD_NAME) { + String fieldName = parser.currentName(); + parser.nextToken(); + values.put(fieldName, context.readValue(parser, Object.class)); + parser.nextToken(); + } + if (parser.currentToken() != JsonToken.END_OBJECT) { + return context.reportInputMismatch( + EmptyUnion.class, "Expected the end of a JSON object while deserializing a union"); + } + return new Unknown(type, values); + } + } + public interface Visitor { T visitUnknown(@Safe String unknownType, Object unknownValue); diff --git a/conjure-java-core/src/integrationInput/java/sealedunions/com/palantir/product/NestedEmptyUnion.java b/conjure-java-core/src/integrationInput/java/sealedunions/com/palantir/product/NestedEmptyUnion.java index 2e1bfd8c4..a27667a87 100644 --- a/conjure-java-core/src/integrationInput/java/sealedunions/com/palantir/product/NestedEmptyUnion.java +++ b/conjure-java-core/src/integrationInput/java/sealedunions/com/palantir/product/NestedEmptyUnion.java @@ -6,13 +6,26 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.annotation.JsonSetter; -import com.fasterxml.jackson.annotation.JsonSubTypes; -import com.fasterxml.jackson.annotation.JsonTypeInfo; import com.fasterxml.jackson.annotation.JsonTypeName; +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.JsonToken; +import com.fasterxml.jackson.core.util.JsonParserSequence; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonDeserializer; +import com.fasterxml.jackson.databind.JsonMappingException; +import com.fasterxml.jackson.databind.JsonSerializer; +import com.fasterxml.jackson.databind.MapperFeature; +import com.fasterxml.jackson.databind.SerializerProvider; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import com.fasterxml.jackson.databind.deser.ResolvableDeserializer; +import com.fasterxml.jackson.databind.util.TokenBuffer; import com.palantir.logsafe.Preconditions; import com.palantir.logsafe.Safe; import com.palantir.logsafe.SafeArg; import com.palantir.logsafe.exceptions.SafeIllegalArgumentException; +import java.io.IOException; import java.util.Collections; import java.util.HashMap; import java.util.Map; @@ -23,13 +36,8 @@ import javax.annotation.processing.Generated; @Generated("com.palantir.conjure.java.types.UnionGenerator") -@JsonTypeInfo( - use = JsonTypeInfo.Id.NAME, - include = JsonTypeInfo.As.EXISTING_PROPERTY, - property = "type", - visible = true, - defaultImpl = NestedEmptyUnion.Unknown.class) -@JsonSubTypes(@JsonSubTypes.Type(value = NestedEmptyUnion.Empty.class, name = "empty")) +@JsonDeserialize(using = NestedEmptyUnion.Deserializer.class) +@JsonSerialize(using = NestedEmptyUnion.Serializer.class) @JsonIgnoreProperties(ignoreUnknown = true) public abstract sealed class NestedEmptyUnion permits NestedEmptyUnion.Empty, NestedEmptyUnion.Unknown { public static NestedEmptyUnion empty(EmptyObject value) { @@ -61,6 +69,9 @@ public Known throwOnUnknown() { public sealed interface Known permits Empty {} @JsonTypeName("empty") + @JsonIgnoreProperties(ignoreUnknown = true) + @JsonDeserialize + @JsonSerialize public static final class Empty extends NestedEmptyUnion implements Known { private final EmptyObject value; @@ -105,6 +116,8 @@ public String toString() { } } + @JsonDeserialize + @JsonSerialize public static final class Unknown extends NestedEmptyUnion { private final String type; @@ -165,6 +178,137 @@ public String toString() { } } + static final class Serializer extends JsonSerializer { + @Override + public void serialize(NestedEmptyUnion value, JsonGenerator generator, SerializerProvider serializers) + throws IOException { + serializers.findValueSerializer(value.getClass()).serialize(value, generator, serializers); + } + } + + static final class Deserializer extends JsonDeserializer implements ResolvableDeserializer { + private static final Class[] VARIANT_TYPES = new Class[] {Empty.class}; + + private volatile JsonDeserializer[] deserializers; + + @Override + public void resolve(DeserializationContext context) throws JsonMappingException { + deserializers = new JsonDeserializer[VARIANT_TYPES.length]; + } + + @Override + public boolean isCachable() { + return true; + } + + @Override + public NestedEmptyUnion deserialize(JsonParser parser, DeserializationContext context) throws IOException { + if (!parser.isExpectedStartObjectToken()) { + return context.reportInputMismatch( + NestedEmptyUnion.class, "Expected a JSON object for union deserialization"); + } + JsonToken firstToken = parser.nextToken(); + if (firstToken == JsonToken.FIELD_NAME && isTypeField(parser.currentName(), context)) { + if (parser.nextToken() != JsonToken.VALUE_STRING) { + return context.reportInputMismatch( + NestedEmptyUnion.class, "Union discriminator 'type' must be a string"); + } + String type = parser.getText(); + parser.nextToken(); + return deserializeSelected(parser, context, type); + } + return deserializeBuffered(parser, context); + } + + private NestedEmptyUnion deserializeBuffered(JsonParser parser, DeserializationContext context) + throws IOException { + try (TokenBuffer buffer = context.bufferForInputBuffering(parser)) { + buffer.writeStartObject(); + JsonToken token = parser.currentToken(); + while (token == JsonToken.FIELD_NAME) { + String fieldName = parser.currentName(); + JsonToken valueToken = parser.nextToken(); + if (isTypeField(fieldName, context)) { + if (valueToken != JsonToken.VALUE_STRING) { + return context.reportInputMismatch( + NestedEmptyUnion.class, "Union discriminator 'type' must be a string"); + } + String type = parser.getText(); + parser.nextToken(); + try (JsonParser bufferedParser = buffer.asParser(parser)) { + JsonParser combinedParser = + JsonParserSequence.createFlattened(true, bufferedParser, parser); + combinedParser.nextToken(); + return deserializeSelected(combinedParser, context, type); + } + } + buffer.writeFieldName(fieldName); + buffer.copyCurrentStructure(parser); + token = parser.nextToken(); + } + if (token != JsonToken.END_OBJECT) { + return context.reportInputMismatch( + NestedEmptyUnion.class, "Expected the end of a JSON object while deserializing a union"); + } + } + return context.reportInputMismatch(NestedEmptyUnion.class, "Union discriminator 'type' is required"); + } + + private static boolean isTypeField(String fieldName, DeserializationContext context) { + return "type".equals(fieldName) + || (context.isEnabled(MapperFeature.ACCEPT_CASE_INSENSITIVE_PROPERTIES) + && "type".equalsIgnoreCase(fieldName)); + } + + private NestedEmptyUnion deserializeSelected(JsonParser parser, DeserializationContext context, String type) + throws IOException { + int variantIndex = + switch (type) { + case "empty" -> 0; + default -> -1; + }; + if (variantIndex < 0) { + return deserializeUnknown(parser, context, type); + } + JsonDeserializer deserializer = deserializers[variantIndex]; + if (deserializer == null) { + deserializer = resolveDeserializer(context, variantIndex); + } + return (NestedEmptyUnion) deserializer.deserialize(parser, context); + } + + private synchronized JsonDeserializer resolveDeserializer(DeserializationContext context, int variantIndex) + throws JsonMappingException { + JsonDeserializer deserializer = deserializers[variantIndex]; + if (deserializer == null) { + deserializer = context.findRootValueDeserializer(context.constructType(VARIANT_TYPES[variantIndex])); + JsonDeserializer[] updated = deserializers.clone(); + updated[variantIndex] = deserializer; + deserializers = updated; + } + return deserializer; + } + + private static NestedEmptyUnion deserializeUnknown( + JsonParser parser, DeserializationContext context, String type) throws IOException { + Map values = new HashMap<>(); + if (parser.currentToken() == JsonToken.START_OBJECT) { + parser.nextToken(); + } + while (parser.currentToken() == JsonToken.FIELD_NAME) { + String fieldName = parser.currentName(); + parser.nextToken(); + values.put(fieldName, context.readValue(parser, Object.class)); + parser.nextToken(); + } + if (parser.currentToken() != JsonToken.END_OBJECT) { + return context.reportInputMismatch( + NestedEmptyUnion.class, "Expected the end of a JSON object while deserializing a union"); + } + return new Unknown(type, values); + } + } + public interface Visitor { T visitEmpty(EmptyObject value); diff --git a/conjure-java-core/src/integrationInput/java/sealedunions/com/palantir/product/SimpleUnion.java b/conjure-java-core/src/integrationInput/java/sealedunions/com/palantir/product/SimpleUnion.java index 7651f6410..96946ab20 100644 --- a/conjure-java-core/src/integrationInput/java/sealedunions/com/palantir/product/SimpleUnion.java +++ b/conjure-java-core/src/integrationInput/java/sealedunions/com/palantir/product/SimpleUnion.java @@ -6,14 +6,27 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.annotation.JsonSetter; -import com.fasterxml.jackson.annotation.JsonSubTypes; -import com.fasterxml.jackson.annotation.JsonTypeInfo; import com.fasterxml.jackson.annotation.JsonTypeName; +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.JsonToken; +import com.fasterxml.jackson.core.util.JsonParserSequence; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonDeserializer; +import com.fasterxml.jackson.databind.JsonMappingException; +import com.fasterxml.jackson.databind.JsonSerializer; +import com.fasterxml.jackson.databind.MapperFeature; +import com.fasterxml.jackson.databind.SerializerProvider; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import com.fasterxml.jackson.databind.deser.ResolvableDeserializer; +import com.fasterxml.jackson.databind.util.TokenBuffer; import com.palantir.conjure.java.lib.SafeLong; import com.palantir.logsafe.Preconditions; import com.palantir.logsafe.Safe; import com.palantir.logsafe.SafeArg; import com.palantir.logsafe.exceptions.SafeIllegalArgumentException; +import java.io.IOException; import java.util.Collections; import java.util.HashMap; import java.util.Map; @@ -25,17 +38,8 @@ import javax.annotation.processing.Generated; @Generated("com.palantir.conjure.java.types.UnionGenerator") -@JsonTypeInfo( - use = JsonTypeInfo.Id.NAME, - include = JsonTypeInfo.As.EXISTING_PROPERTY, - property = "type", - visible = true, - defaultImpl = SimpleUnion.Unknown.class) -@JsonSubTypes({ - @JsonSubTypes.Type(value = SimpleUnion.Foo.class, name = "foo"), - @JsonSubTypes.Type(value = SimpleUnion.Bar.class, name = "bar"), - @JsonSubTypes.Type(value = SimpleUnion.Baz.class, name = "baz") -}) +@JsonDeserialize(using = SimpleUnion.Deserializer.class) +@JsonSerialize(using = SimpleUnion.Serializer.class) @JsonIgnoreProperties(ignoreUnknown = true) public abstract sealed class SimpleUnion permits SimpleUnion.Foo, SimpleUnion.Bar, SimpleUnion.Baz, SimpleUnion.Unknown { @@ -81,6 +85,9 @@ public Known throwOnUnknown() { public sealed interface Known permits Foo, Bar, Baz {} @JsonTypeName("foo") + @JsonIgnoreProperties(ignoreUnknown = true) + @JsonDeserialize + @JsonSerialize public static final class Foo extends SimpleUnion implements Known { private final String value; @@ -126,6 +133,9 @@ public String toString() { } @JsonTypeName("bar") + @JsonIgnoreProperties(ignoreUnknown = true) + @JsonDeserialize + @JsonSerialize public static final class Bar extends SimpleUnion implements Known { private final int value; @@ -171,6 +181,9 @@ public String toString() { } @JsonTypeName("baz") + @JsonIgnoreProperties(ignoreUnknown = true) + @JsonDeserialize + @JsonSerialize public static final class Baz extends SimpleUnion implements Known { private final SafeLong value; @@ -215,6 +228,8 @@ public String toString() { } } + @JsonDeserialize + @JsonSerialize public static final class Unknown extends SimpleUnion { private final String type; @@ -275,6 +290,138 @@ public String toString() { } } + static final class Serializer extends JsonSerializer { + @Override + public void serialize(SimpleUnion value, JsonGenerator generator, SerializerProvider serializers) + throws IOException { + serializers.findValueSerializer(value.getClass()).serialize(value, generator, serializers); + } + } + + static final class Deserializer extends JsonDeserializer implements ResolvableDeserializer { + private static final Class[] VARIANT_TYPES = new Class[] {Foo.class, Bar.class, Baz.class}; + + private volatile JsonDeserializer[] deserializers; + + @Override + public void resolve(DeserializationContext context) throws JsonMappingException { + deserializers = new JsonDeserializer[VARIANT_TYPES.length]; + } + + @Override + public boolean isCachable() { + return true; + } + + @Override + public SimpleUnion deserialize(JsonParser parser, DeserializationContext context) throws IOException { + if (!parser.isExpectedStartObjectToken()) { + return context.reportInputMismatch( + SimpleUnion.class, "Expected a JSON object for union deserialization"); + } + JsonToken firstToken = parser.nextToken(); + if (firstToken == JsonToken.FIELD_NAME && isTypeField(parser.currentName(), context)) { + if (parser.nextToken() != JsonToken.VALUE_STRING) { + return context.reportInputMismatch( + SimpleUnion.class, "Union discriminator 'type' must be a string"); + } + String type = parser.getText(); + parser.nextToken(); + return deserializeSelected(parser, context, type); + } + return deserializeBuffered(parser, context); + } + + private SimpleUnion deserializeBuffered(JsonParser parser, DeserializationContext context) throws IOException { + try (TokenBuffer buffer = context.bufferForInputBuffering(parser)) { + buffer.writeStartObject(); + JsonToken token = parser.currentToken(); + while (token == JsonToken.FIELD_NAME) { + String fieldName = parser.currentName(); + JsonToken valueToken = parser.nextToken(); + if (isTypeField(fieldName, context)) { + if (valueToken != JsonToken.VALUE_STRING) { + return context.reportInputMismatch( + SimpleUnion.class, "Union discriminator 'type' must be a string"); + } + String type = parser.getText(); + parser.nextToken(); + try (JsonParser bufferedParser = buffer.asParser(parser)) { + JsonParser combinedParser = + JsonParserSequence.createFlattened(true, bufferedParser, parser); + combinedParser.nextToken(); + return deserializeSelected(combinedParser, context, type); + } + } + buffer.writeFieldName(fieldName); + buffer.copyCurrentStructure(parser); + token = parser.nextToken(); + } + if (token != JsonToken.END_OBJECT) { + return context.reportInputMismatch( + SimpleUnion.class, "Expected the end of a JSON object while deserializing a union"); + } + } + return context.reportInputMismatch(SimpleUnion.class, "Union discriminator 'type' is required"); + } + + private static boolean isTypeField(String fieldName, DeserializationContext context) { + return "type".equals(fieldName) + || (context.isEnabled(MapperFeature.ACCEPT_CASE_INSENSITIVE_PROPERTIES) + && "type".equalsIgnoreCase(fieldName)); + } + + private SimpleUnion deserializeSelected(JsonParser parser, DeserializationContext context, String type) + throws IOException { + int variantIndex = + switch (type) { + case "foo" -> 0; + case "bar" -> 1; + case "baz" -> 2; + default -> -1; + }; + if (variantIndex < 0) { + return deserializeUnknown(parser, context, type); + } + JsonDeserializer deserializer = deserializers[variantIndex]; + if (deserializer == null) { + deserializer = resolveDeserializer(context, variantIndex); + } + return (SimpleUnion) deserializer.deserialize(parser, context); + } + + private synchronized JsonDeserializer resolveDeserializer(DeserializationContext context, int variantIndex) + throws JsonMappingException { + JsonDeserializer deserializer = deserializers[variantIndex]; + if (deserializer == null) { + deserializer = context.findRootValueDeserializer(context.constructType(VARIANT_TYPES[variantIndex])); + JsonDeserializer[] updated = deserializers.clone(); + updated[variantIndex] = deserializer; + deserializers = updated; + } + return deserializer; + } + + private static SimpleUnion deserializeUnknown(JsonParser parser, DeserializationContext context, String type) + throws IOException { + Map values = new HashMap<>(); + if (parser.currentToken() == JsonToken.START_OBJECT) { + parser.nextToken(); + } + while (parser.currentToken() == JsonToken.FIELD_NAME) { + String fieldName = parser.currentName(); + parser.nextToken(); + values.put(fieldName, context.readValue(parser, Object.class)); + parser.nextToken(); + } + if (parser.currentToken() != JsonToken.END_OBJECT) { + return context.reportInputMismatch( + SimpleUnion.class, "Expected the end of a JSON object while deserializing a union"); + } + return new Unknown(type, values); + } + } + public interface Visitor { T visitFoo(String value); diff --git a/conjure-java-core/src/integrationInput/java/sealedunions/com/palantir/product/UnionReservedNames.java b/conjure-java-core/src/integrationInput/java/sealedunions/com/palantir/product/UnionReservedNames.java index 9d2c43e9b..5ad67c9c8 100644 --- a/conjure-java-core/src/integrationInput/java/sealedunions/com/palantir/product/UnionReservedNames.java +++ b/conjure-java-core/src/integrationInput/java/sealedunions/com/palantir/product/UnionReservedNames.java @@ -6,13 +6,26 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.annotation.JsonSetter; -import com.fasterxml.jackson.annotation.JsonSubTypes; -import com.fasterxml.jackson.annotation.JsonTypeInfo; import com.fasterxml.jackson.annotation.JsonTypeName; +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.JsonToken; +import com.fasterxml.jackson.core.util.JsonParserSequence; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonDeserializer; +import com.fasterxml.jackson.databind.JsonMappingException; +import com.fasterxml.jackson.databind.JsonSerializer; +import com.fasterxml.jackson.databind.MapperFeature; +import com.fasterxml.jackson.databind.SerializerProvider; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import com.fasterxml.jackson.databind.deser.ResolvableDeserializer; +import com.fasterxml.jackson.databind.util.TokenBuffer; import com.palantir.logsafe.Preconditions; import com.palantir.logsafe.Safe; import com.palantir.logsafe.SafeArg; import com.palantir.logsafe.exceptions.SafeIllegalArgumentException; +import java.io.IOException; import java.util.Collections; import java.util.HashMap; import java.util.Map; @@ -23,29 +36,8 @@ import javax.annotation.processing.Generated; @Generated("com.palantir.conjure.java.types.UnionGenerator") -@JsonTypeInfo( - use = JsonTypeInfo.Id.NAME, - include = JsonTypeInfo.As.EXISTING_PROPERTY, - property = "type", - visible = true, - defaultImpl = UnionReservedNames.Unknown.class) -@JsonSubTypes({ - @JsonSubTypes.Type(value = UnionReservedNames.Known_.class, name = "known"), - @JsonSubTypes.Type(value = UnionReservedNames.Unknown_.class, name = "unknown"), - @JsonSubTypes.Type(value = UnionReservedNames.If.class, name = "if"), - @JsonSubTypes.Type(value = UnionReservedNames.New.class, name = "new"), - @JsonSubTypes.Type(value = UnionReservedNames.Interface.class, name = "interface"), - @JsonSubTypes.Type(value = UnionReservedNames.Void.class, name = "void"), - @JsonSubTypes.Type(value = UnionReservedNames.Return.class, name = "return"), - @JsonSubTypes.Type(value = UnionReservedNames.Private.class, name = "private"), - @JsonSubTypes.Type(value = UnionReservedNames.Public.class, name = "public"), - @JsonSubTypes.Type(value = UnionReservedNames.Int.class, name = "int"), - @JsonSubTypes.Type(value = UnionReservedNames.Import.class, name = "import"), - @JsonSubTypes.Type(value = UnionReservedNames.Final.class, name = "final"), - @JsonSubTypes.Type(value = UnionReservedNames.Throws.class, name = "throws"), - @JsonSubTypes.Type(value = UnionReservedNames.Static.class, name = "static"), - @JsonSubTypes.Type(value = UnionReservedNames.UnionReservedNames_.class, name = "unionReservedNames") -}) +@JsonDeserialize(using = UnionReservedNames.Deserializer.class) +@JsonSerialize(using = UnionReservedNames.Serializer.class) @JsonIgnoreProperties(ignoreUnknown = true) public abstract sealed class UnionReservedNames permits UnionReservedNames.Known_, @@ -206,6 +198,9 @@ public sealed interface Known UnionReservedNames_ {} @JsonTypeName("known") + @JsonIgnoreProperties(ignoreUnknown = true) + @JsonDeserialize + @JsonSerialize public static final class Known_ extends UnionReservedNames implements Known { private final String value; @@ -251,6 +246,9 @@ public String toString() { } @JsonTypeName("unknown") + @JsonIgnoreProperties(ignoreUnknown = true) + @JsonDeserialize + @JsonSerialize public static final class Unknown_ extends UnionReservedNames implements Known { private final String value; @@ -296,6 +294,9 @@ public String toString() { } @JsonTypeName("if") + @JsonIgnoreProperties(ignoreUnknown = true) + @JsonDeserialize + @JsonSerialize public static final class If extends UnionReservedNames implements Known { private final String value; @@ -341,6 +342,9 @@ public String toString() { } @JsonTypeName("new") + @JsonIgnoreProperties(ignoreUnknown = true) + @JsonDeserialize + @JsonSerialize public static final class New extends UnionReservedNames implements Known { private final String value; @@ -386,6 +390,9 @@ public String toString() { } @JsonTypeName("interface") + @JsonIgnoreProperties(ignoreUnknown = true) + @JsonDeserialize + @JsonSerialize public static final class Interface extends UnionReservedNames implements Known { private final String value; @@ -431,6 +438,9 @@ public String toString() { } @JsonTypeName("void") + @JsonIgnoreProperties(ignoreUnknown = true) + @JsonDeserialize + @JsonSerialize public static final class Void extends UnionReservedNames implements Known { private final String value; @@ -476,6 +486,9 @@ public String toString() { } @JsonTypeName("return") + @JsonIgnoreProperties(ignoreUnknown = true) + @JsonDeserialize + @JsonSerialize public static final class Return extends UnionReservedNames implements Known { private final String value; @@ -521,6 +534,9 @@ public String toString() { } @JsonTypeName("private") + @JsonIgnoreProperties(ignoreUnknown = true) + @JsonDeserialize + @JsonSerialize public static final class Private extends UnionReservedNames implements Known { private final String value; @@ -566,6 +582,9 @@ public String toString() { } @JsonTypeName("public") + @JsonIgnoreProperties(ignoreUnknown = true) + @JsonDeserialize + @JsonSerialize public static final class Public extends UnionReservedNames implements Known { private final String value; @@ -611,6 +630,9 @@ public String toString() { } @JsonTypeName("int") + @JsonIgnoreProperties(ignoreUnknown = true) + @JsonDeserialize + @JsonSerialize public static final class Int extends UnionReservedNames implements Known { private final String value; @@ -656,6 +678,9 @@ public String toString() { } @JsonTypeName("import") + @JsonIgnoreProperties(ignoreUnknown = true) + @JsonDeserialize + @JsonSerialize public static final class Import extends UnionReservedNames implements Known { private final String value; @@ -701,6 +726,9 @@ public String toString() { } @JsonTypeName("final") + @JsonIgnoreProperties(ignoreUnknown = true) + @JsonDeserialize + @JsonSerialize public static final class Final extends UnionReservedNames implements Known { private final String value; @@ -746,6 +774,9 @@ public String toString() { } @JsonTypeName("throws") + @JsonIgnoreProperties(ignoreUnknown = true) + @JsonDeserialize + @JsonSerialize public static final class Throws extends UnionReservedNames implements Known { private final String value; @@ -791,6 +822,9 @@ public String toString() { } @JsonTypeName("static") + @JsonIgnoreProperties(ignoreUnknown = true) + @JsonDeserialize + @JsonSerialize public static final class Static extends UnionReservedNames implements Known { private final String value; @@ -836,6 +870,9 @@ public String toString() { } @JsonTypeName("unionReservedNames") + @JsonIgnoreProperties(ignoreUnknown = true) + @JsonDeserialize + @JsonSerialize public static final class UnionReservedNames_ extends UnionReservedNames implements Known { private final String value; @@ -880,6 +917,8 @@ public String toString() { } } + @JsonDeserialize + @JsonSerialize public static final class Unknown extends UnionReservedNames { private final String type; @@ -940,6 +979,167 @@ public String toString() { } } + static final class Serializer extends JsonSerializer { + @Override + public void serialize(UnionReservedNames value, JsonGenerator generator, SerializerProvider serializers) + throws IOException { + serializers.findValueSerializer(value.getClass()).serialize(value, generator, serializers); + } + } + + static final class Deserializer extends JsonDeserializer implements ResolvableDeserializer { + private static final Class[] VARIANT_TYPES = new Class[] { + Known_.class, + Unknown_.class, + If.class, + New.class, + Interface.class, + Void.class, + Return.class, + Private.class, + Public.class, + Int.class, + Import.class, + Final.class, + Throws.class, + Static.class, + UnionReservedNames_.class + }; + + private volatile JsonDeserializer[] deserializers; + + @Override + public void resolve(DeserializationContext context) throws JsonMappingException { + deserializers = new JsonDeserializer[VARIANT_TYPES.length]; + } + + @Override + public boolean isCachable() { + return true; + } + + @Override + public UnionReservedNames deserialize(JsonParser parser, DeserializationContext context) throws IOException { + if (!parser.isExpectedStartObjectToken()) { + return context.reportInputMismatch( + UnionReservedNames.class, "Expected a JSON object for union deserialization"); + } + JsonToken firstToken = parser.nextToken(); + if (firstToken == JsonToken.FIELD_NAME && isTypeField(parser.currentName(), context)) { + if (parser.nextToken() != JsonToken.VALUE_STRING) { + return context.reportInputMismatch( + UnionReservedNames.class, "Union discriminator 'type' must be a string"); + } + String type = parser.getText(); + parser.nextToken(); + return deserializeSelected(parser, context, type); + } + return deserializeBuffered(parser, context); + } + + private UnionReservedNames deserializeBuffered(JsonParser parser, DeserializationContext context) + throws IOException { + try (TokenBuffer buffer = context.bufferForInputBuffering(parser)) { + buffer.writeStartObject(); + JsonToken token = parser.currentToken(); + while (token == JsonToken.FIELD_NAME) { + String fieldName = parser.currentName(); + JsonToken valueToken = parser.nextToken(); + if (isTypeField(fieldName, context)) { + if (valueToken != JsonToken.VALUE_STRING) { + return context.reportInputMismatch( + UnionReservedNames.class, "Union discriminator 'type' must be a string"); + } + String type = parser.getText(); + parser.nextToken(); + try (JsonParser bufferedParser = buffer.asParser(parser)) { + JsonParser combinedParser = + JsonParserSequence.createFlattened(true, bufferedParser, parser); + combinedParser.nextToken(); + return deserializeSelected(combinedParser, context, type); + } + } + buffer.writeFieldName(fieldName); + buffer.copyCurrentStructure(parser); + token = parser.nextToken(); + } + if (token != JsonToken.END_OBJECT) { + return context.reportInputMismatch( + UnionReservedNames.class, "Expected the end of a JSON object while deserializing a union"); + } + } + return context.reportInputMismatch(UnionReservedNames.class, "Union discriminator 'type' is required"); + } + + private static boolean isTypeField(String fieldName, DeserializationContext context) { + return "type".equals(fieldName) + || (context.isEnabled(MapperFeature.ACCEPT_CASE_INSENSITIVE_PROPERTIES) + && "type".equalsIgnoreCase(fieldName)); + } + + private UnionReservedNames deserializeSelected(JsonParser parser, DeserializationContext context, String type) + throws IOException { + int variantIndex = + switch (type) { + case "known" -> 0; + case "unknown" -> 1; + case "if" -> 2; + case "new" -> 3; + case "interface" -> 4; + case "void" -> 5; + case "return" -> 6; + case "private" -> 7; + case "public" -> 8; + case "int" -> 9; + case "import" -> 10; + case "final" -> 11; + case "throws" -> 12; + case "static" -> 13; + case "unionReservedNames" -> 14; + default -> -1; + }; + if (variantIndex < 0) { + return deserializeUnknown(parser, context, type); + } + JsonDeserializer deserializer = deserializers[variantIndex]; + if (deserializer == null) { + deserializer = resolveDeserializer(context, variantIndex); + } + return (UnionReservedNames) deserializer.deserialize(parser, context); + } + + private synchronized JsonDeserializer resolveDeserializer(DeserializationContext context, int variantIndex) + throws JsonMappingException { + JsonDeserializer deserializer = deserializers[variantIndex]; + if (deserializer == null) { + deserializer = context.findRootValueDeserializer(context.constructType(VARIANT_TYPES[variantIndex])); + JsonDeserializer[] updated = deserializers.clone(); + updated[variantIndex] = deserializer; + deserializers = updated; + } + return deserializer; + } + + private static UnionReservedNames deserializeUnknown( + JsonParser parser, DeserializationContext context, String type) throws IOException { + Map values = new HashMap<>(); + if (parser.currentToken() == JsonToken.START_OBJECT) { + parser.nextToken(); + } + while (parser.currentToken() == JsonToken.FIELD_NAME) { + String fieldName = parser.currentName(); + parser.nextToken(); + values.put(fieldName, context.readValue(parser, Object.class)); + parser.nextToken(); + } + if (parser.currentToken() != JsonToken.END_OBJECT) { + return context.reportInputMismatch( + UnionReservedNames.class, "Expected the end of a JSON object while deserializing a union"); + } + return new Unknown(type, values); + } + } + public interface Visitor { T visitKnown(String value); diff --git a/conjure-java-core/src/integrationInput/java/servicevanilla/com/palantir/product/datasets/BackingFileSystem.java b/conjure-java-core/src/integrationInput/java/servicevanilla/com/palantir/product/datasets/BackingFileSystem.java index 6765c2924..9010f0830 100644 --- a/conjure-java-core/src/integrationInput/java/servicevanilla/com/palantir/product/datasets/BackingFileSystem.java +++ b/conjure-java-core/src/integrationInput/java/servicevanilla/com/palantir/product/datasets/BackingFileSystem.java @@ -131,6 +131,7 @@ public static final class Builder { private String baseUri; + @JsonSetter(value = "configuration", nulls = Nulls.SKIP) private Map configuration = new LinkedHashMap<>(); private Builder() {} @@ -158,7 +159,6 @@ public Builder baseUri(@Nonnull String baseUri) { return this; } - @JsonSetter(value = "configuration", nulls = Nulls.SKIP) public Builder configuration(@Nonnull Map configuration) { checkNotBuilt(); this.configuration = diff --git a/conjure-java-core/src/integrationInput/java/stagedbuilder/com/palantir/product/MultipleFieldsOneFinalStage.java b/conjure-java-core/src/integrationInput/java/stagedbuilder/com/palantir/product/MultipleFieldsOneFinalStage.java index 23c19e716..2ea8dd384 100644 --- a/conjure-java-core/src/integrationInput/java/stagedbuilder/com/palantir/product/MultipleFieldsOneFinalStage.java +++ b/conjure-java-core/src/integrationInput/java/stagedbuilder/com/palantir/product/MultipleFieldsOneFinalStage.java @@ -195,6 +195,7 @@ public interface Builder extends TokenStageBuilder, Completed_StageBuilder { static final class DefaultBuilder implements Builder { boolean _buildInvoked; + @JsonSetter(value = "mappedRids", nulls = Nulls.SKIP) private Map mappedRids = new LinkedHashMap<>(); private OneField token; @@ -213,7 +214,6 @@ public Builder from(MultipleFieldsOneFinalStage other) { } @Override - @JsonSetter(value = "mappedRids", nulls = Nulls.SKIP) public Builder mappedRids(@Nonnull Map mappedRids) { checkNotBuilt(); this.mappedRids = new LinkedHashMap<>(Preconditions.checkNotNull(mappedRids, "mappedRids cannot be null")); diff --git a/conjure-java-core/src/integrationInput/java/stagedbuilder/com/palantir/product/MultipleFieldsOnlyFinalStage.java b/conjure-java-core/src/integrationInput/java/stagedbuilder/com/palantir/product/MultipleFieldsOnlyFinalStage.java index f41afeafb..4290183f3 100644 --- a/conjure-java-core/src/integrationInput/java/stagedbuilder/com/palantir/product/MultipleFieldsOnlyFinalStage.java +++ b/conjure-java-core/src/integrationInput/java/stagedbuilder/com/palantir/product/MultipleFieldsOnlyFinalStage.java @@ -207,6 +207,7 @@ public static final class Builder { private List items = ConjureCollections.newList(); + @JsonSetter(value = "itemsMap", nulls = Nulls.SKIP) private Map itemsMap = new LinkedHashMap<>(); private Optional optionalItem = Optional.empty(); @@ -215,6 +216,7 @@ public static final class Builder { private List itemsOld = ConjureCollections.newList(); + @JsonSetter(value = "itemsMapOld", nulls = Nulls.SKIP) private Map itemsMapOld = new LinkedHashMap<>(); private Optional optionalItemOld = Optional.empty(); @@ -255,7 +257,6 @@ public Builder items(String items) { return this; } - @JsonSetter(value = "itemsMap", nulls = Nulls.SKIP) public Builder itemsMap(@Nonnull Map itemsMap) { checkNotBuilt(); this.itemsMap = new LinkedHashMap<>(Preconditions.checkNotNull(itemsMap, "itemsMap cannot be null")); @@ -333,7 +334,6 @@ public Builder itemsOld(String itemsOld) { /** @deprecated this map is deprecated */ @Deprecated - @JsonSetter(value = "itemsMapOld", nulls = Nulls.SKIP) public Builder itemsMapOld(@Nonnull Map itemsMapOld) { checkNotBuilt(); this.itemsMapOld = diff --git a/conjure-java-core/src/integrationInput/java/stagedbuilder/com/palantir/product/MultipleOrderedStages.java b/conjure-java-core/src/integrationInput/java/stagedbuilder/com/palantir/product/MultipleOrderedStages.java index a1289bc69..c1a14270d 100644 --- a/conjure-java-core/src/integrationInput/java/stagedbuilder/com/palantir/product/MultipleOrderedStages.java +++ b/conjure-java-core/src/integrationInput/java/stagedbuilder/com/palantir/product/MultipleOrderedStages.java @@ -246,6 +246,7 @@ static final class DefaultBuilder implements Builder { private Set items = ConjureCollections.newSet(); + @JsonSetter(value = "mappedRids", nulls = Nulls.SKIP) private Map mappedRids = new LinkedHashMap<>(); private Optional optionalItem = Optional.empty(); @@ -302,7 +303,6 @@ public Builder items(SafeLong items) { } @Override - @JsonSetter(value = "mappedRids", nulls = Nulls.SKIP) public Builder mappedRids(@Nonnull Map mappedRids) { checkNotBuilt(); this.mappedRids = new LinkedHashMap<>(Preconditions.checkNotNull(mappedRids, "mappedRids cannot be null")); diff --git a/conjure-java-core/src/integrationInput/java/strictstagedbuilder/com/palantir/product/StrictFourFields.java b/conjure-java-core/src/integrationInput/java/strictstagedbuilder/com/palantir/product/StrictFourFields.java index 60420e4b2..8d8be3d06 100644 --- a/conjure-java-core/src/integrationInput/java/strictstagedbuilder/com/palantir/product/StrictFourFields.java +++ b/conjure-java-core/src/integrationInput/java/strictstagedbuilder/com/palantir/product/StrictFourFields.java @@ -206,6 +206,7 @@ static final class DefaultBuilder implements Builder { private Optional optionalItem = Optional.empty(); + @JsonSetter(value = "mappedRids", nulls = Nulls.SKIP) private Map mappedRids = new LinkedHashMap<>(); private DefaultBuilder() {} @@ -252,7 +253,6 @@ public Builder optionalItem(@Nonnull String optionalItem) { } @Override - @JsonSetter(value = "mappedRids", nulls = Nulls.SKIP) public Builder mappedRids(@Nonnull Map mappedRids) { checkNotBuilt(); this.mappedRids = new LinkedHashMap<>(Preconditions.checkNotNull(mappedRids, "mappedRids cannot be null")); diff --git a/conjure-java-core/src/integrationInput/java/strictstagedbuilder/com/palantir/product/StrictMultipleDeprecatedAndUnsafeFields.java b/conjure-java-core/src/integrationInput/java/strictstagedbuilder/com/palantir/product/StrictMultipleDeprecatedAndUnsafeFields.java index b426e42d9..bc58171fa 100644 --- a/conjure-java-core/src/integrationInput/java/strictstagedbuilder/com/palantir/product/StrictMultipleDeprecatedAndUnsafeFields.java +++ b/conjure-java-core/src/integrationInput/java/strictstagedbuilder/com/palantir/product/StrictMultipleDeprecatedAndUnsafeFields.java @@ -287,6 +287,7 @@ static final class DefaultBuilder implements Builder { private Optional<@Unsafe String> optionalItem = Optional.empty(); + @JsonSetter(value = "mappedRids", nulls = Nulls.SKIP) private Map mappedRids = new LinkedHashMap<>(); private StrictFourFields strictFourFieldsObject; @@ -365,7 +366,6 @@ public Builder optionalItem(@Nonnull @Unsafe String optionalItem) { } @Override - @JsonSetter(value = "mappedRids", nulls = Nulls.SKIP) public Builder mappedRids(@Nonnull Map mappedRids) { checkNotBuilt(); this.mappedRids = new LinkedHashMap<>(Preconditions.checkNotNull(mappedRids, "mappedRids cannot be null")); diff --git a/conjure-java-core/src/integrationInput/java/template/com/palantir/product/AliasAsMapKeyExample.java b/conjure-java-core/src/integrationInput/java/template/com/palantir/product/AliasAsMapKeyExample.java index bdc4985da..0efbefb7e 100644 --- a/conjure-java-core/src/integrationInput/java/template/com/palantir/product/AliasAsMapKeyExample.java +++ b/conjure-java-core/src/integrationInput/java/template/com/palantir/product/AliasAsMapKeyExample.java @@ -178,18 +178,25 @@ public static Builder builder() { public static final class Builder { boolean _buildInvoked; + @JsonSetter(value = "strings", nulls = Nulls.SKIP, contentNulls = Nulls.FAIL) private Map strings = new LinkedHashMap<>(); + @JsonSetter(value = "rids", nulls = Nulls.SKIP, contentNulls = Nulls.FAIL) private Map rids = new LinkedHashMap<>(); + @JsonSetter(value = "bearertokens", nulls = Nulls.SKIP, contentNulls = Nulls.FAIL) private Map bearertokens = new LinkedHashMap<>(); + @JsonSetter(value = "integers", nulls = Nulls.SKIP, contentNulls = Nulls.FAIL) private Map integers = new LinkedHashMap<>(); + @JsonSetter(value = "safelongs", nulls = Nulls.SKIP, contentNulls = Nulls.FAIL) private Map safelongs = new LinkedHashMap<>(); + @JsonSetter(value = "datetimes", nulls = Nulls.SKIP, contentNulls = Nulls.FAIL) private Map datetimes = new LinkedHashMap<>(); + @JsonSetter(value = "uuids", nulls = Nulls.SKIP, contentNulls = Nulls.FAIL) private Map uuids = new LinkedHashMap<>(); private Builder() {} @@ -206,7 +213,6 @@ public Builder from(AliasAsMapKeyExample other) { return this; } - @JsonSetter(value = "strings", nulls = Nulls.SKIP, contentNulls = Nulls.FAIL) public Builder strings(@Nonnull Map strings) { checkNotBuilt(); this.strings = new LinkedHashMap<>(Preconditions.checkNotNull(strings, "strings cannot be null")); @@ -225,7 +231,6 @@ public Builder strings(StringAliasExample key, ManyFieldExample value) { return this; } - @JsonSetter(value = "rids", nulls = Nulls.SKIP, contentNulls = Nulls.FAIL) public Builder rids(@Nonnull Map rids) { checkNotBuilt(); this.rids = new LinkedHashMap<>(Preconditions.checkNotNull(rids, "rids cannot be null")); @@ -244,7 +249,6 @@ public Builder rids(RidAliasExample key, ManyFieldExample value) { return this; } - @JsonSetter(value = "bearertokens", nulls = Nulls.SKIP, contentNulls = Nulls.FAIL) public Builder bearertokens(@Nonnull Map bearertokens) { checkNotBuilt(); this.bearertokens = @@ -264,7 +268,6 @@ public Builder bearertokens(BearerTokenAliasExample key, ManyFieldExample value) return this; } - @JsonSetter(value = "integers", nulls = Nulls.SKIP, contentNulls = Nulls.FAIL) public Builder integers(@Nonnull Map integers) { checkNotBuilt(); this.integers = new LinkedHashMap<>(Preconditions.checkNotNull(integers, "integers cannot be null")); @@ -283,7 +286,6 @@ public Builder integers(IntegerAliasExample key, ManyFieldExample value) { return this; } - @JsonSetter(value = "safelongs", nulls = Nulls.SKIP, contentNulls = Nulls.FAIL) public Builder safelongs(@Nonnull Map safelongs) { checkNotBuilt(); this.safelongs = new LinkedHashMap<>(Preconditions.checkNotNull(safelongs, "safelongs cannot be null")); @@ -302,7 +304,6 @@ public Builder safelongs(SafeLongAliasExample key, ManyFieldExample value) { return this; } - @JsonSetter(value = "datetimes", nulls = Nulls.SKIP, contentNulls = Nulls.FAIL) public Builder datetimes(@Nonnull Map datetimes) { checkNotBuilt(); this.datetimes = new LinkedHashMap<>(Preconditions.checkNotNull(datetimes, "datetimes cannot be null")); @@ -321,7 +322,6 @@ public Builder datetimes(DateTimeAliasExample key, ManyFieldExample value) { return this; } - @JsonSetter(value = "uuids", nulls = Nulls.SKIP, contentNulls = Nulls.FAIL) public Builder uuids(@Nonnull Map uuids) { checkNotBuilt(); this.uuids = new LinkedHashMap<>(Preconditions.checkNotNull(uuids, "uuids cannot be null")); diff --git a/conjure-java-core/src/integrationInput/java/template/com/palantir/product/AnyMapExample.java b/conjure-java-core/src/integrationInput/java/template/com/palantir/product/AnyMapExample.java index 949df69be..22763acce 100644 --- a/conjure-java-core/src/integrationInput/java/template/com/palantir/product/AnyMapExample.java +++ b/conjure-java-core/src/integrationInput/java/template/com/palantir/product/AnyMapExample.java @@ -91,6 +91,7 @@ public static Builder builder() { public static final class Builder { boolean _buildInvoked; + @JsonSetter(value = "items", nulls = Nulls.SKIP, contentNulls = Nulls.FAIL) private Map items = new LinkedHashMap<>(); private Builder() {} @@ -101,7 +102,6 @@ public Builder from(AnyMapExample other) { return this; } - @JsonSetter(value = "items", nulls = Nulls.SKIP, contentNulls = Nulls.FAIL) public Builder items(@Nonnull Map items) { checkNotBuilt(); this.items = new LinkedHashMap<>(Preconditions.checkNotNull(items, "items cannot be null")); diff --git a/conjure-java-core/src/integrationInput/java/template/com/palantir/product/EmptyUnionTypeExample.java b/conjure-java-core/src/integrationInput/java/template/com/palantir/product/EmptyUnionTypeExample.java index f15ae80de..9512fa137 100644 --- a/conjure-java-core/src/integrationInput/java/template/com/palantir/product/EmptyUnionTypeExample.java +++ b/conjure-java-core/src/integrationInput/java/template/com/palantir/product/EmptyUnionTypeExample.java @@ -3,14 +3,23 @@ import com.fasterxml.jackson.annotation.JsonAnyGetter; import com.fasterxml.jackson.annotation.JsonAnySetter; import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.annotation.JsonTypeInfo; import com.fasterxml.jackson.annotation.JsonValue; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.JsonToken; +import com.fasterxml.jackson.core.util.JsonParserSequence; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonDeserializer; +import com.fasterxml.jackson.databind.JsonMappingException; +import com.fasterxml.jackson.databind.MapperFeature; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.deser.ResolvableDeserializer; +import com.fasterxml.jackson.databind.util.TokenBuffer; import com.palantir.logsafe.Preconditions; import com.palantir.logsafe.Safe; import com.palantir.logsafe.SafeArg; import com.palantir.logsafe.exceptions.SafeIllegalArgumentException; +import java.io.IOException; import java.util.Collections; import java.util.HashMap; import java.util.Map; @@ -20,6 +29,7 @@ import javax.annotation.processing.Generated; @Generated("com.palantir.conjure.java.types.UnionGenerator") +@JsonDeserialize(using = EmptyUnionTypeExample.Deserializer.class) public final class EmptyUnionTypeExample { private final Base value; @@ -113,13 +123,6 @@ public interface Completed_StageVisitorBuilder { Visitor build(); } - @JsonTypeInfo( - use = JsonTypeInfo.Id.NAME, - include = JsonTypeInfo.As.EXISTING_PROPERTY, - property = "type", - visible = true, - defaultImpl = UnknownWrapper.class) - @JsonIgnoreProperties(ignoreUnknown = true) private interface Base { T accept(Visitor visitor); } @@ -183,4 +186,127 @@ public String toString() { return "UnknownWrapper{type: " + type + ", value: " + value + '}'; } } + + static final class Deserializer extends JsonDeserializer implements ResolvableDeserializer { + private static final Class[] VARIANT_TYPES = new Class[] {}; + + private volatile JsonDeserializer[] deserializers; + + @Override + public void resolve(DeserializationContext context) throws JsonMappingException { + deserializers = new JsonDeserializer[VARIANT_TYPES.length]; + } + + @Override + public boolean isCachable() { + return true; + } + + @Override + public EmptyUnionTypeExample deserialize(JsonParser parser, DeserializationContext context) throws IOException { + if (!parser.isExpectedStartObjectToken()) { + return context.reportInputMismatch( + EmptyUnionTypeExample.class, "Expected a JSON object for union deserialization"); + } + JsonToken firstToken = parser.nextToken(); + if (firstToken == JsonToken.FIELD_NAME && isTypeField(parser.currentName(), context)) { + if (parser.nextToken() != JsonToken.VALUE_STRING) { + return context.reportInputMismatch( + EmptyUnionTypeExample.class, "Union discriminator 'type' must be a string"); + } + String type = parser.getText(); + parser.nextToken(); + return deserializeSelected(parser, context, type); + } + return deserializeBuffered(parser, context); + } + + private EmptyUnionTypeExample deserializeBuffered(JsonParser parser, DeserializationContext context) + throws IOException { + try (TokenBuffer buffer = context.bufferForInputBuffering(parser)) { + buffer.writeStartObject(); + JsonToken token = parser.currentToken(); + while (token == JsonToken.FIELD_NAME) { + String fieldName = parser.currentName(); + JsonToken valueToken = parser.nextToken(); + if (isTypeField(fieldName, context)) { + if (valueToken != JsonToken.VALUE_STRING) { + return context.reportInputMismatch( + EmptyUnionTypeExample.class, "Union discriminator 'type' must be a string"); + } + String type = parser.getText(); + parser.nextToken(); + try (JsonParser bufferedParser = buffer.asParser(parser)) { + JsonParser combinedParser = + JsonParserSequence.createFlattened(true, bufferedParser, parser); + combinedParser.nextToken(); + return deserializeSelected(combinedParser, context, type); + } + } + buffer.writeFieldName(fieldName); + buffer.copyCurrentStructure(parser); + token = parser.nextToken(); + } + if (token != JsonToken.END_OBJECT) { + return context.reportInputMismatch( + EmptyUnionTypeExample.class, + "Expected the end of a JSON object while deserializing a union"); + } + } + return context.reportInputMismatch(EmptyUnionTypeExample.class, "Union discriminator 'type' is required"); + } + + private static boolean isTypeField(String fieldName, DeserializationContext context) { + return "type".equals(fieldName) + || (context.isEnabled(MapperFeature.ACCEPT_CASE_INSENSITIVE_PROPERTIES) + && "type".equalsIgnoreCase(fieldName)); + } + + private EmptyUnionTypeExample deserializeSelected( + JsonParser parser, DeserializationContext context, String type) throws IOException { + int variantIndex = + switch (type) { + default -> -1; + }; + if (variantIndex < 0) { + return deserializeUnknown(parser, context, type); + } + JsonDeserializer deserializer = deserializers[variantIndex]; + if (deserializer == null) { + deserializer = resolveDeserializer(context, variantIndex); + } + return new EmptyUnionTypeExample((Base) deserializer.deserialize(parser, context)); + } + + private synchronized JsonDeserializer resolveDeserializer(DeserializationContext context, int variantIndex) + throws JsonMappingException { + JsonDeserializer deserializer = deserializers[variantIndex]; + if (deserializer == null) { + deserializer = context.findRootValueDeserializer(context.constructType(VARIANT_TYPES[variantIndex])); + JsonDeserializer[] updated = deserializers.clone(); + updated[variantIndex] = deserializer; + deserializers = updated; + } + return deserializer; + } + + private static EmptyUnionTypeExample deserializeUnknown( + JsonParser parser, DeserializationContext context, String type) throws IOException { + Map values = new HashMap<>(); + if (parser.currentToken() == JsonToken.START_OBJECT) { + parser.nextToken(); + } + while (parser.currentToken() == JsonToken.FIELD_NAME) { + String fieldName = parser.currentName(); + parser.nextToken(); + values.put(fieldName, context.readValue(parser, Object.class)); + parser.nextToken(); + } + if (parser.currentToken() != JsonToken.END_OBJECT) { + return context.reportInputMismatch( + EmptyUnionTypeExample.class, "Expected the end of a JSON object while deserializing a union"); + } + return new EmptyUnionTypeExample(new UnknownWrapper(type, values)); + } + } } diff --git a/conjure-java-core/src/integrationInput/java/template/com/palantir/product/ExternalLongUnionExample.java b/conjure-java-core/src/integrationInput/java/template/com/palantir/product/ExternalLongUnionExample.java index 05779ce06..6e2d94371 100644 --- a/conjure-java-core/src/integrationInput/java/template/com/palantir/product/ExternalLongUnionExample.java +++ b/conjure-java-core/src/integrationInput/java/template/com/palantir/product/ExternalLongUnionExample.java @@ -6,14 +6,23 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.annotation.JsonSetter; -import com.fasterxml.jackson.annotation.JsonSubTypes; -import com.fasterxml.jackson.annotation.JsonTypeInfo; import com.fasterxml.jackson.annotation.JsonTypeName; import com.fasterxml.jackson.annotation.JsonValue; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.JsonToken; +import com.fasterxml.jackson.core.util.JsonParserSequence; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonDeserializer; +import com.fasterxml.jackson.databind.JsonMappingException; +import com.fasterxml.jackson.databind.MapperFeature; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.deser.ResolvableDeserializer; +import com.fasterxml.jackson.databind.util.TokenBuffer; import com.palantir.logsafe.Preconditions; import com.palantir.logsafe.Safe; import com.palantir.logsafe.SafeArg; import com.palantir.logsafe.exceptions.SafeIllegalArgumentException; +import java.io.IOException; import java.util.Collections; import java.util.HashMap; import java.util.Map; @@ -24,6 +33,7 @@ /** A union of a safe long. */ @Generated("com.palantir.conjure.java.types.UnionGenerator") +@JsonDeserialize(using = ExternalLongUnionExample.Deserializer.class) public final class ExternalLongUnionExample { private final Base value; @@ -147,19 +157,12 @@ public interface Completed_StageVisitorBuilder { Visitor build(); } - @JsonTypeInfo( - use = JsonTypeInfo.Id.NAME, - include = JsonTypeInfo.As.EXISTING_PROPERTY, - property = "type", - visible = true, - defaultImpl = UnknownWrapper.class) - @JsonSubTypes(@JsonSubTypes.Type(SafeLongWrapper.class)) - @JsonIgnoreProperties(ignoreUnknown = true) private interface Base { T accept(Visitor visitor); } @JsonTypeName("safeLong") + @JsonIgnoreProperties(ignoreUnknown = true) private static final class SafeLongWrapper implements Base { private final long value; @@ -263,4 +266,132 @@ public String toString() { return "UnknownWrapper{type: " + type + ", value: " + value + '}'; } } + + static final class Deserializer extends JsonDeserializer + implements ResolvableDeserializer { + private static final Class[] VARIANT_TYPES = new Class[] {SafeLongWrapper.class}; + + private volatile JsonDeserializer[] deserializers; + + @Override + public void resolve(DeserializationContext context) throws JsonMappingException { + deserializers = new JsonDeserializer[VARIANT_TYPES.length]; + } + + @Override + public boolean isCachable() { + return true; + } + + @Override + public ExternalLongUnionExample deserialize(JsonParser parser, DeserializationContext context) + throws IOException { + if (!parser.isExpectedStartObjectToken()) { + return context.reportInputMismatch( + ExternalLongUnionExample.class, "Expected a JSON object for union deserialization"); + } + JsonToken firstToken = parser.nextToken(); + if (firstToken == JsonToken.FIELD_NAME && isTypeField(parser.currentName(), context)) { + if (parser.nextToken() != JsonToken.VALUE_STRING) { + return context.reportInputMismatch( + ExternalLongUnionExample.class, "Union discriminator 'type' must be a string"); + } + String type = parser.getText(); + parser.nextToken(); + return deserializeSelected(parser, context, type); + } + return deserializeBuffered(parser, context); + } + + private ExternalLongUnionExample deserializeBuffered(JsonParser parser, DeserializationContext context) + throws IOException { + try (TokenBuffer buffer = context.bufferForInputBuffering(parser)) { + buffer.writeStartObject(); + JsonToken token = parser.currentToken(); + while (token == JsonToken.FIELD_NAME) { + String fieldName = parser.currentName(); + JsonToken valueToken = parser.nextToken(); + if (isTypeField(fieldName, context)) { + if (valueToken != JsonToken.VALUE_STRING) { + return context.reportInputMismatch( + ExternalLongUnionExample.class, "Union discriminator 'type' must be a string"); + } + String type = parser.getText(); + parser.nextToken(); + try (JsonParser bufferedParser = buffer.asParser(parser)) { + JsonParser combinedParser = + JsonParserSequence.createFlattened(true, bufferedParser, parser); + combinedParser.nextToken(); + return deserializeSelected(combinedParser, context, type); + } + } + buffer.writeFieldName(fieldName); + buffer.copyCurrentStructure(parser); + token = parser.nextToken(); + } + if (token != JsonToken.END_OBJECT) { + return context.reportInputMismatch( + ExternalLongUnionExample.class, + "Expected the end of a JSON object while deserializing a union"); + } + } + return context.reportInputMismatch( + ExternalLongUnionExample.class, "Union discriminator 'type' is required"); + } + + private static boolean isTypeField(String fieldName, DeserializationContext context) { + return "type".equals(fieldName) + || (context.isEnabled(MapperFeature.ACCEPT_CASE_INSENSITIVE_PROPERTIES) + && "type".equalsIgnoreCase(fieldName)); + } + + private ExternalLongUnionExample deserializeSelected( + JsonParser parser, DeserializationContext context, String type) throws IOException { + int variantIndex = + switch (type) { + case "safeLong" -> 0; + default -> -1; + }; + if (variantIndex < 0) { + return deserializeUnknown(parser, context, type); + } + JsonDeserializer deserializer = deserializers[variantIndex]; + if (deserializer == null) { + deserializer = resolveDeserializer(context, variantIndex); + } + return new ExternalLongUnionExample((Base) deserializer.deserialize(parser, context)); + } + + private synchronized JsonDeserializer resolveDeserializer(DeserializationContext context, int variantIndex) + throws JsonMappingException { + JsonDeserializer deserializer = deserializers[variantIndex]; + if (deserializer == null) { + deserializer = context.findRootValueDeserializer(context.constructType(VARIANT_TYPES[variantIndex])); + JsonDeserializer[] updated = deserializers.clone(); + updated[variantIndex] = deserializer; + deserializers = updated; + } + return deserializer; + } + + private static ExternalLongUnionExample deserializeUnknown( + JsonParser parser, DeserializationContext context, String type) throws IOException { + Map values = new HashMap<>(); + if (parser.currentToken() == JsonToken.START_OBJECT) { + parser.nextToken(); + } + while (parser.currentToken() == JsonToken.FIELD_NAME) { + String fieldName = parser.currentName(); + parser.nextToken(); + values.put(fieldName, context.readValue(parser, Object.class)); + parser.nextToken(); + } + if (parser.currentToken() != JsonToken.END_OBJECT) { + return context.reportInputMismatch( + ExternalLongUnionExample.class, + "Expected the end of a JSON object while deserializing a union"); + } + return new ExternalLongUnionExample(new UnknownWrapper(type, values)); + } + } } diff --git a/conjure-java-core/src/integrationInput/java/template/com/palantir/product/LargeUnionExample.java b/conjure-java-core/src/integrationInput/java/template/com/palantir/product/LargeUnionExample.java index d8060a480..d44851b16 100644 --- a/conjure-java-core/src/integrationInput/java/template/com/palantir/product/LargeUnionExample.java +++ b/conjure-java-core/src/integrationInput/java/template/com/palantir/product/LargeUnionExample.java @@ -6,13 +6,22 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.annotation.JsonSetter; -import com.fasterxml.jackson.annotation.JsonSubTypes; -import com.fasterxml.jackson.annotation.JsonTypeInfo; import com.fasterxml.jackson.annotation.JsonTypeName; import com.fasterxml.jackson.annotation.JsonValue; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.JsonToken; +import com.fasterxml.jackson.core.util.JsonParserSequence; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonDeserializer; +import com.fasterxml.jackson.databind.JsonMappingException; +import com.fasterxml.jackson.databind.MapperFeature; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.deser.ResolvableDeserializer; +import com.fasterxml.jackson.databind.util.TokenBuffer; import com.palantir.logsafe.Preconditions; import com.palantir.logsafe.Safe; import com.palantir.logsafe.exceptions.SafeIllegalArgumentException; +import java.io.IOException; import java.util.Collections; import java.util.HashMap; import java.util.Map; @@ -21,6 +30,7 @@ import javax.annotation.processing.Generated; @Generated("com.palantir.conjure.java.types.UnionGenerator") +@JsonDeserialize(using = LargeUnionExample.Deserializer.class) public final class LargeUnionExample { private final Base value; @@ -977,121 +987,12 @@ public interface Visitor { T visitUnknown(@Safe String unknownType); } - @JsonTypeInfo( - use = JsonTypeInfo.Id.NAME, - include = JsonTypeInfo.As.EXISTING_PROPERTY, - property = "type", - visible = true, - defaultImpl = UnknownWrapper.class) - @JsonSubTypes({ - @JsonSubTypes.Type(U0Wrapper.class), - @JsonSubTypes.Type(U1Wrapper.class), - @JsonSubTypes.Type(U2Wrapper.class), - @JsonSubTypes.Type(U3Wrapper.class), - @JsonSubTypes.Type(U4Wrapper.class), - @JsonSubTypes.Type(U5Wrapper.class), - @JsonSubTypes.Type(U6Wrapper.class), - @JsonSubTypes.Type(U7Wrapper.class), - @JsonSubTypes.Type(U8Wrapper.class), - @JsonSubTypes.Type(U9Wrapper.class), - @JsonSubTypes.Type(U10Wrapper.class), - @JsonSubTypes.Type(U11Wrapper.class), - @JsonSubTypes.Type(U12Wrapper.class), - @JsonSubTypes.Type(U13Wrapper.class), - @JsonSubTypes.Type(U14Wrapper.class), - @JsonSubTypes.Type(U15Wrapper.class), - @JsonSubTypes.Type(U16Wrapper.class), - @JsonSubTypes.Type(U17Wrapper.class), - @JsonSubTypes.Type(U18Wrapper.class), - @JsonSubTypes.Type(U19Wrapper.class), - @JsonSubTypes.Type(U20Wrapper.class), - @JsonSubTypes.Type(U21Wrapper.class), - @JsonSubTypes.Type(U22Wrapper.class), - @JsonSubTypes.Type(U23Wrapper.class), - @JsonSubTypes.Type(U24Wrapper.class), - @JsonSubTypes.Type(U25Wrapper.class), - @JsonSubTypes.Type(U26Wrapper.class), - @JsonSubTypes.Type(U27Wrapper.class), - @JsonSubTypes.Type(U28Wrapper.class), - @JsonSubTypes.Type(U29Wrapper.class), - @JsonSubTypes.Type(U30Wrapper.class), - @JsonSubTypes.Type(U31Wrapper.class), - @JsonSubTypes.Type(U32Wrapper.class), - @JsonSubTypes.Type(U33Wrapper.class), - @JsonSubTypes.Type(U34Wrapper.class), - @JsonSubTypes.Type(U35Wrapper.class), - @JsonSubTypes.Type(U36Wrapper.class), - @JsonSubTypes.Type(U37Wrapper.class), - @JsonSubTypes.Type(U38Wrapper.class), - @JsonSubTypes.Type(U39Wrapper.class), - @JsonSubTypes.Type(U40Wrapper.class), - @JsonSubTypes.Type(U41Wrapper.class), - @JsonSubTypes.Type(U42Wrapper.class), - @JsonSubTypes.Type(U43Wrapper.class), - @JsonSubTypes.Type(U44Wrapper.class), - @JsonSubTypes.Type(U45Wrapper.class), - @JsonSubTypes.Type(U46Wrapper.class), - @JsonSubTypes.Type(U47Wrapper.class), - @JsonSubTypes.Type(U48Wrapper.class), - @JsonSubTypes.Type(U49Wrapper.class), - @JsonSubTypes.Type(U50Wrapper.class), - @JsonSubTypes.Type(U51Wrapper.class), - @JsonSubTypes.Type(U52Wrapper.class), - @JsonSubTypes.Type(U53Wrapper.class), - @JsonSubTypes.Type(U54Wrapper.class), - @JsonSubTypes.Type(U55Wrapper.class), - @JsonSubTypes.Type(U56Wrapper.class), - @JsonSubTypes.Type(U57Wrapper.class), - @JsonSubTypes.Type(U58Wrapper.class), - @JsonSubTypes.Type(U59Wrapper.class), - @JsonSubTypes.Type(U60Wrapper.class), - @JsonSubTypes.Type(U61Wrapper.class), - @JsonSubTypes.Type(U62Wrapper.class), - @JsonSubTypes.Type(U63Wrapper.class), - @JsonSubTypes.Type(U64Wrapper.class), - @JsonSubTypes.Type(U65Wrapper.class), - @JsonSubTypes.Type(U66Wrapper.class), - @JsonSubTypes.Type(U67Wrapper.class), - @JsonSubTypes.Type(U68Wrapper.class), - @JsonSubTypes.Type(U69Wrapper.class), - @JsonSubTypes.Type(U70Wrapper.class), - @JsonSubTypes.Type(U71Wrapper.class), - @JsonSubTypes.Type(U72Wrapper.class), - @JsonSubTypes.Type(U73Wrapper.class), - @JsonSubTypes.Type(U74Wrapper.class), - @JsonSubTypes.Type(U75Wrapper.class), - @JsonSubTypes.Type(U76Wrapper.class), - @JsonSubTypes.Type(U77Wrapper.class), - @JsonSubTypes.Type(U78Wrapper.class), - @JsonSubTypes.Type(U79Wrapper.class), - @JsonSubTypes.Type(U80Wrapper.class), - @JsonSubTypes.Type(U81Wrapper.class), - @JsonSubTypes.Type(U82Wrapper.class), - @JsonSubTypes.Type(U83Wrapper.class), - @JsonSubTypes.Type(U84Wrapper.class), - @JsonSubTypes.Type(U85Wrapper.class), - @JsonSubTypes.Type(U86Wrapper.class), - @JsonSubTypes.Type(U87Wrapper.class), - @JsonSubTypes.Type(U88Wrapper.class), - @JsonSubTypes.Type(U89Wrapper.class), - @JsonSubTypes.Type(U90Wrapper.class), - @JsonSubTypes.Type(U91Wrapper.class), - @JsonSubTypes.Type(U92Wrapper.class), - @JsonSubTypes.Type(U93Wrapper.class), - @JsonSubTypes.Type(U94Wrapper.class), - @JsonSubTypes.Type(U95Wrapper.class), - @JsonSubTypes.Type(U96Wrapper.class), - @JsonSubTypes.Type(U97Wrapper.class), - @JsonSubTypes.Type(U98Wrapper.class), - @JsonSubTypes.Type(U99Wrapper.class), - @JsonSubTypes.Type(U100Wrapper.class) - }) - @JsonIgnoreProperties(ignoreUnknown = true) private interface Base { T accept(Visitor visitor); } @JsonTypeName("u0") + @JsonIgnoreProperties(ignoreUnknown = true) private static final class U0Wrapper implements Base { private final String value; @@ -1137,6 +1038,7 @@ public String toString() { } @JsonTypeName("u1") + @JsonIgnoreProperties(ignoreUnknown = true) private static final class U1Wrapper implements Base { private final String value; @@ -1182,6 +1084,7 @@ public String toString() { } @JsonTypeName("u2") + @JsonIgnoreProperties(ignoreUnknown = true) private static final class U2Wrapper implements Base { private final String value; @@ -1227,6 +1130,7 @@ public String toString() { } @JsonTypeName("u3") + @JsonIgnoreProperties(ignoreUnknown = true) private static final class U3Wrapper implements Base { private final String value; @@ -1272,6 +1176,7 @@ public String toString() { } @JsonTypeName("u4") + @JsonIgnoreProperties(ignoreUnknown = true) private static final class U4Wrapper implements Base { private final String value; @@ -1317,6 +1222,7 @@ public String toString() { } @JsonTypeName("u5") + @JsonIgnoreProperties(ignoreUnknown = true) private static final class U5Wrapper implements Base { private final String value; @@ -1362,6 +1268,7 @@ public String toString() { } @JsonTypeName("u6") + @JsonIgnoreProperties(ignoreUnknown = true) private static final class U6Wrapper implements Base { private final String value; @@ -1407,6 +1314,7 @@ public String toString() { } @JsonTypeName("u7") + @JsonIgnoreProperties(ignoreUnknown = true) private static final class U7Wrapper implements Base { private final String value; @@ -1452,6 +1360,7 @@ public String toString() { } @JsonTypeName("u8") + @JsonIgnoreProperties(ignoreUnknown = true) private static final class U8Wrapper implements Base { private final String value; @@ -1497,6 +1406,7 @@ public String toString() { } @JsonTypeName("u9") + @JsonIgnoreProperties(ignoreUnknown = true) private static final class U9Wrapper implements Base { private final String value; @@ -1542,6 +1452,7 @@ public String toString() { } @JsonTypeName("u10") + @JsonIgnoreProperties(ignoreUnknown = true) private static final class U10Wrapper implements Base { private final String value; @@ -1587,6 +1498,7 @@ public String toString() { } @JsonTypeName("u11") + @JsonIgnoreProperties(ignoreUnknown = true) private static final class U11Wrapper implements Base { private final String value; @@ -1632,6 +1544,7 @@ public String toString() { } @JsonTypeName("u12") + @JsonIgnoreProperties(ignoreUnknown = true) private static final class U12Wrapper implements Base { private final String value; @@ -1677,6 +1590,7 @@ public String toString() { } @JsonTypeName("u13") + @JsonIgnoreProperties(ignoreUnknown = true) private static final class U13Wrapper implements Base { private final String value; @@ -1722,6 +1636,7 @@ public String toString() { } @JsonTypeName("u14") + @JsonIgnoreProperties(ignoreUnknown = true) private static final class U14Wrapper implements Base { private final String value; @@ -1767,6 +1682,7 @@ public String toString() { } @JsonTypeName("u15") + @JsonIgnoreProperties(ignoreUnknown = true) private static final class U15Wrapper implements Base { private final String value; @@ -1812,6 +1728,7 @@ public String toString() { } @JsonTypeName("u16") + @JsonIgnoreProperties(ignoreUnknown = true) private static final class U16Wrapper implements Base { private final String value; @@ -1857,6 +1774,7 @@ public String toString() { } @JsonTypeName("u17") + @JsonIgnoreProperties(ignoreUnknown = true) private static final class U17Wrapper implements Base { private final String value; @@ -1902,6 +1820,7 @@ public String toString() { } @JsonTypeName("u18") + @JsonIgnoreProperties(ignoreUnknown = true) private static final class U18Wrapper implements Base { private final String value; @@ -1947,6 +1866,7 @@ public String toString() { } @JsonTypeName("u19") + @JsonIgnoreProperties(ignoreUnknown = true) private static final class U19Wrapper implements Base { private final String value; @@ -1992,6 +1912,7 @@ public String toString() { } @JsonTypeName("u20") + @JsonIgnoreProperties(ignoreUnknown = true) private static final class U20Wrapper implements Base { private final String value; @@ -2037,6 +1958,7 @@ public String toString() { } @JsonTypeName("u21") + @JsonIgnoreProperties(ignoreUnknown = true) private static final class U21Wrapper implements Base { private final String value; @@ -2082,6 +2004,7 @@ public String toString() { } @JsonTypeName("u22") + @JsonIgnoreProperties(ignoreUnknown = true) private static final class U22Wrapper implements Base { private final String value; @@ -2127,6 +2050,7 @@ public String toString() { } @JsonTypeName("u23") + @JsonIgnoreProperties(ignoreUnknown = true) private static final class U23Wrapper implements Base { private final String value; @@ -2172,6 +2096,7 @@ public String toString() { } @JsonTypeName("u24") + @JsonIgnoreProperties(ignoreUnknown = true) private static final class U24Wrapper implements Base { private final String value; @@ -2217,6 +2142,7 @@ public String toString() { } @JsonTypeName("u25") + @JsonIgnoreProperties(ignoreUnknown = true) private static final class U25Wrapper implements Base { private final String value; @@ -2262,6 +2188,7 @@ public String toString() { } @JsonTypeName("u26") + @JsonIgnoreProperties(ignoreUnknown = true) private static final class U26Wrapper implements Base { private final String value; @@ -2307,6 +2234,7 @@ public String toString() { } @JsonTypeName("u27") + @JsonIgnoreProperties(ignoreUnknown = true) private static final class U27Wrapper implements Base { private final String value; @@ -2352,6 +2280,7 @@ public String toString() { } @JsonTypeName("u28") + @JsonIgnoreProperties(ignoreUnknown = true) private static final class U28Wrapper implements Base { private final String value; @@ -2397,6 +2326,7 @@ public String toString() { } @JsonTypeName("u29") + @JsonIgnoreProperties(ignoreUnknown = true) private static final class U29Wrapper implements Base { private final String value; @@ -2442,6 +2372,7 @@ public String toString() { } @JsonTypeName("u30") + @JsonIgnoreProperties(ignoreUnknown = true) private static final class U30Wrapper implements Base { private final String value; @@ -2487,6 +2418,7 @@ public String toString() { } @JsonTypeName("u31") + @JsonIgnoreProperties(ignoreUnknown = true) private static final class U31Wrapper implements Base { private final String value; @@ -2532,6 +2464,7 @@ public String toString() { } @JsonTypeName("u32") + @JsonIgnoreProperties(ignoreUnknown = true) private static final class U32Wrapper implements Base { private final String value; @@ -2577,6 +2510,7 @@ public String toString() { } @JsonTypeName("u33") + @JsonIgnoreProperties(ignoreUnknown = true) private static final class U33Wrapper implements Base { private final String value; @@ -2622,6 +2556,7 @@ public String toString() { } @JsonTypeName("u34") + @JsonIgnoreProperties(ignoreUnknown = true) private static final class U34Wrapper implements Base { private final String value; @@ -2667,6 +2602,7 @@ public String toString() { } @JsonTypeName("u35") + @JsonIgnoreProperties(ignoreUnknown = true) private static final class U35Wrapper implements Base { private final String value; @@ -2712,6 +2648,7 @@ public String toString() { } @JsonTypeName("u36") + @JsonIgnoreProperties(ignoreUnknown = true) private static final class U36Wrapper implements Base { private final String value; @@ -2757,6 +2694,7 @@ public String toString() { } @JsonTypeName("u37") + @JsonIgnoreProperties(ignoreUnknown = true) private static final class U37Wrapper implements Base { private final String value; @@ -2802,6 +2740,7 @@ public String toString() { } @JsonTypeName("u38") + @JsonIgnoreProperties(ignoreUnknown = true) private static final class U38Wrapper implements Base { private final String value; @@ -2847,6 +2786,7 @@ public String toString() { } @JsonTypeName("u39") + @JsonIgnoreProperties(ignoreUnknown = true) private static final class U39Wrapper implements Base { private final String value; @@ -2892,6 +2832,7 @@ public String toString() { } @JsonTypeName("u40") + @JsonIgnoreProperties(ignoreUnknown = true) private static final class U40Wrapper implements Base { private final String value; @@ -2937,6 +2878,7 @@ public String toString() { } @JsonTypeName("u41") + @JsonIgnoreProperties(ignoreUnknown = true) private static final class U41Wrapper implements Base { private final String value; @@ -2982,6 +2924,7 @@ public String toString() { } @JsonTypeName("u42") + @JsonIgnoreProperties(ignoreUnknown = true) private static final class U42Wrapper implements Base { private final String value; @@ -3027,6 +2970,7 @@ public String toString() { } @JsonTypeName("u43") + @JsonIgnoreProperties(ignoreUnknown = true) private static final class U43Wrapper implements Base { private final String value; @@ -3072,6 +3016,7 @@ public String toString() { } @JsonTypeName("u44") + @JsonIgnoreProperties(ignoreUnknown = true) private static final class U44Wrapper implements Base { private final String value; @@ -3117,6 +3062,7 @@ public String toString() { } @JsonTypeName("u45") + @JsonIgnoreProperties(ignoreUnknown = true) private static final class U45Wrapper implements Base { private final String value; @@ -3162,6 +3108,7 @@ public String toString() { } @JsonTypeName("u46") + @JsonIgnoreProperties(ignoreUnknown = true) private static final class U46Wrapper implements Base { private final String value; @@ -3207,6 +3154,7 @@ public String toString() { } @JsonTypeName("u47") + @JsonIgnoreProperties(ignoreUnknown = true) private static final class U47Wrapper implements Base { private final String value; @@ -3252,6 +3200,7 @@ public String toString() { } @JsonTypeName("u48") + @JsonIgnoreProperties(ignoreUnknown = true) private static final class U48Wrapper implements Base { private final String value; @@ -3297,6 +3246,7 @@ public String toString() { } @JsonTypeName("u49") + @JsonIgnoreProperties(ignoreUnknown = true) private static final class U49Wrapper implements Base { private final String value; @@ -3342,6 +3292,7 @@ public String toString() { } @JsonTypeName("u50") + @JsonIgnoreProperties(ignoreUnknown = true) private static final class U50Wrapper implements Base { private final String value; @@ -3387,6 +3338,7 @@ public String toString() { } @JsonTypeName("u51") + @JsonIgnoreProperties(ignoreUnknown = true) private static final class U51Wrapper implements Base { private final String value; @@ -3432,6 +3384,7 @@ public String toString() { } @JsonTypeName("u52") + @JsonIgnoreProperties(ignoreUnknown = true) private static final class U52Wrapper implements Base { private final String value; @@ -3477,6 +3430,7 @@ public String toString() { } @JsonTypeName("u53") + @JsonIgnoreProperties(ignoreUnknown = true) private static final class U53Wrapper implements Base { private final String value; @@ -3522,6 +3476,7 @@ public String toString() { } @JsonTypeName("u54") + @JsonIgnoreProperties(ignoreUnknown = true) private static final class U54Wrapper implements Base { private final String value; @@ -3567,6 +3522,7 @@ public String toString() { } @JsonTypeName("u55") + @JsonIgnoreProperties(ignoreUnknown = true) private static final class U55Wrapper implements Base { private final String value; @@ -3612,6 +3568,7 @@ public String toString() { } @JsonTypeName("u56") + @JsonIgnoreProperties(ignoreUnknown = true) private static final class U56Wrapper implements Base { private final String value; @@ -3657,6 +3614,7 @@ public String toString() { } @JsonTypeName("u57") + @JsonIgnoreProperties(ignoreUnknown = true) private static final class U57Wrapper implements Base { private final String value; @@ -3702,6 +3660,7 @@ public String toString() { } @JsonTypeName("u58") + @JsonIgnoreProperties(ignoreUnknown = true) private static final class U58Wrapper implements Base { private final String value; @@ -3747,6 +3706,7 @@ public String toString() { } @JsonTypeName("u59") + @JsonIgnoreProperties(ignoreUnknown = true) private static final class U59Wrapper implements Base { private final String value; @@ -3792,6 +3752,7 @@ public String toString() { } @JsonTypeName("u60") + @JsonIgnoreProperties(ignoreUnknown = true) private static final class U60Wrapper implements Base { private final String value; @@ -3837,6 +3798,7 @@ public String toString() { } @JsonTypeName("u61") + @JsonIgnoreProperties(ignoreUnknown = true) private static final class U61Wrapper implements Base { private final String value; @@ -3882,6 +3844,7 @@ public String toString() { } @JsonTypeName("u62") + @JsonIgnoreProperties(ignoreUnknown = true) private static final class U62Wrapper implements Base { private final String value; @@ -3927,6 +3890,7 @@ public String toString() { } @JsonTypeName("u63") + @JsonIgnoreProperties(ignoreUnknown = true) private static final class U63Wrapper implements Base { private final String value; @@ -3972,6 +3936,7 @@ public String toString() { } @JsonTypeName("u64") + @JsonIgnoreProperties(ignoreUnknown = true) private static final class U64Wrapper implements Base { private final String value; @@ -4017,6 +3982,7 @@ public String toString() { } @JsonTypeName("u65") + @JsonIgnoreProperties(ignoreUnknown = true) private static final class U65Wrapper implements Base { private final String value; @@ -4062,6 +4028,7 @@ public String toString() { } @JsonTypeName("u66") + @JsonIgnoreProperties(ignoreUnknown = true) private static final class U66Wrapper implements Base { private final String value; @@ -4107,6 +4074,7 @@ public String toString() { } @JsonTypeName("u67") + @JsonIgnoreProperties(ignoreUnknown = true) private static final class U67Wrapper implements Base { private final String value; @@ -4152,6 +4120,7 @@ public String toString() { } @JsonTypeName("u68") + @JsonIgnoreProperties(ignoreUnknown = true) private static final class U68Wrapper implements Base { private final String value; @@ -4197,6 +4166,7 @@ public String toString() { } @JsonTypeName("u69") + @JsonIgnoreProperties(ignoreUnknown = true) private static final class U69Wrapper implements Base { private final String value; @@ -4242,6 +4212,7 @@ public String toString() { } @JsonTypeName("u70") + @JsonIgnoreProperties(ignoreUnknown = true) private static final class U70Wrapper implements Base { private final String value; @@ -4287,6 +4258,7 @@ public String toString() { } @JsonTypeName("u71") + @JsonIgnoreProperties(ignoreUnknown = true) private static final class U71Wrapper implements Base { private final String value; @@ -4332,6 +4304,7 @@ public String toString() { } @JsonTypeName("u72") + @JsonIgnoreProperties(ignoreUnknown = true) private static final class U72Wrapper implements Base { private final String value; @@ -4377,6 +4350,7 @@ public String toString() { } @JsonTypeName("u73") + @JsonIgnoreProperties(ignoreUnknown = true) private static final class U73Wrapper implements Base { private final String value; @@ -4422,6 +4396,7 @@ public String toString() { } @JsonTypeName("u74") + @JsonIgnoreProperties(ignoreUnknown = true) private static final class U74Wrapper implements Base { private final String value; @@ -4467,6 +4442,7 @@ public String toString() { } @JsonTypeName("u75") + @JsonIgnoreProperties(ignoreUnknown = true) private static final class U75Wrapper implements Base { private final String value; @@ -4512,6 +4488,7 @@ public String toString() { } @JsonTypeName("u76") + @JsonIgnoreProperties(ignoreUnknown = true) private static final class U76Wrapper implements Base { private final String value; @@ -4557,6 +4534,7 @@ public String toString() { } @JsonTypeName("u77") + @JsonIgnoreProperties(ignoreUnknown = true) private static final class U77Wrapper implements Base { private final String value; @@ -4602,6 +4580,7 @@ public String toString() { } @JsonTypeName("u78") + @JsonIgnoreProperties(ignoreUnknown = true) private static final class U78Wrapper implements Base { private final String value; @@ -4647,6 +4626,7 @@ public String toString() { } @JsonTypeName("u79") + @JsonIgnoreProperties(ignoreUnknown = true) private static final class U79Wrapper implements Base { private final String value; @@ -4692,6 +4672,7 @@ public String toString() { } @JsonTypeName("u80") + @JsonIgnoreProperties(ignoreUnknown = true) private static final class U80Wrapper implements Base { private final String value; @@ -4737,6 +4718,7 @@ public String toString() { } @JsonTypeName("u81") + @JsonIgnoreProperties(ignoreUnknown = true) private static final class U81Wrapper implements Base { private final String value; @@ -4782,6 +4764,7 @@ public String toString() { } @JsonTypeName("u82") + @JsonIgnoreProperties(ignoreUnknown = true) private static final class U82Wrapper implements Base { private final String value; @@ -4827,6 +4810,7 @@ public String toString() { } @JsonTypeName("u83") + @JsonIgnoreProperties(ignoreUnknown = true) private static final class U83Wrapper implements Base { private final String value; @@ -4872,6 +4856,7 @@ public String toString() { } @JsonTypeName("u84") + @JsonIgnoreProperties(ignoreUnknown = true) private static final class U84Wrapper implements Base { private final String value; @@ -4917,6 +4902,7 @@ public String toString() { } @JsonTypeName("u85") + @JsonIgnoreProperties(ignoreUnknown = true) private static final class U85Wrapper implements Base { private final String value; @@ -4962,6 +4948,7 @@ public String toString() { } @JsonTypeName("u86") + @JsonIgnoreProperties(ignoreUnknown = true) private static final class U86Wrapper implements Base { private final String value; @@ -5007,6 +4994,7 @@ public String toString() { } @JsonTypeName("u87") + @JsonIgnoreProperties(ignoreUnknown = true) private static final class U87Wrapper implements Base { private final String value; @@ -5052,6 +5040,7 @@ public String toString() { } @JsonTypeName("u88") + @JsonIgnoreProperties(ignoreUnknown = true) private static final class U88Wrapper implements Base { private final String value; @@ -5097,6 +5086,7 @@ public String toString() { } @JsonTypeName("u89") + @JsonIgnoreProperties(ignoreUnknown = true) private static final class U89Wrapper implements Base { private final String value; @@ -5142,6 +5132,7 @@ public String toString() { } @JsonTypeName("u90") + @JsonIgnoreProperties(ignoreUnknown = true) private static final class U90Wrapper implements Base { private final String value; @@ -5187,6 +5178,7 @@ public String toString() { } @JsonTypeName("u91") + @JsonIgnoreProperties(ignoreUnknown = true) private static final class U91Wrapper implements Base { private final String value; @@ -5232,6 +5224,7 @@ public String toString() { } @JsonTypeName("u92") + @JsonIgnoreProperties(ignoreUnknown = true) private static final class U92Wrapper implements Base { private final String value; @@ -5277,6 +5270,7 @@ public String toString() { } @JsonTypeName("u93") + @JsonIgnoreProperties(ignoreUnknown = true) private static final class U93Wrapper implements Base { private final String value; @@ -5322,6 +5316,7 @@ public String toString() { } @JsonTypeName("u94") + @JsonIgnoreProperties(ignoreUnknown = true) private static final class U94Wrapper implements Base { private final String value; @@ -5367,6 +5362,7 @@ public String toString() { } @JsonTypeName("u95") + @JsonIgnoreProperties(ignoreUnknown = true) private static final class U95Wrapper implements Base { private final String value; @@ -5412,6 +5408,7 @@ public String toString() { } @JsonTypeName("u96") + @JsonIgnoreProperties(ignoreUnknown = true) private static final class U96Wrapper implements Base { private final String value; @@ -5457,6 +5454,7 @@ public String toString() { } @JsonTypeName("u97") + @JsonIgnoreProperties(ignoreUnknown = true) private static final class U97Wrapper implements Base { private final String value; @@ -5502,6 +5500,7 @@ public String toString() { } @JsonTypeName("u98") + @JsonIgnoreProperties(ignoreUnknown = true) private static final class U98Wrapper implements Base { private final String value; @@ -5547,6 +5546,7 @@ public String toString() { } @JsonTypeName("u99") + @JsonIgnoreProperties(ignoreUnknown = true) private static final class U99Wrapper implements Base { private final String value; @@ -5592,6 +5592,7 @@ public String toString() { } @JsonTypeName("u100") + @JsonIgnoreProperties(ignoreUnknown = true) private static final class U100Wrapper implements Base { private final String value; @@ -5695,4 +5696,329 @@ public String toString() { return "UnknownWrapper{type: " + type + ", value: " + value + '}'; } } + + static final class Deserializer extends JsonDeserializer implements ResolvableDeserializer { + private static final Class[] VARIANT_TYPES = new Class[] { + U0Wrapper.class, + U1Wrapper.class, + U2Wrapper.class, + U3Wrapper.class, + U4Wrapper.class, + U5Wrapper.class, + U6Wrapper.class, + U7Wrapper.class, + U8Wrapper.class, + U9Wrapper.class, + U10Wrapper.class, + U11Wrapper.class, + U12Wrapper.class, + U13Wrapper.class, + U14Wrapper.class, + U15Wrapper.class, + U16Wrapper.class, + U17Wrapper.class, + U18Wrapper.class, + U19Wrapper.class, + U20Wrapper.class, + U21Wrapper.class, + U22Wrapper.class, + U23Wrapper.class, + U24Wrapper.class, + U25Wrapper.class, + U26Wrapper.class, + U27Wrapper.class, + U28Wrapper.class, + U29Wrapper.class, + U30Wrapper.class, + U31Wrapper.class, + U32Wrapper.class, + U33Wrapper.class, + U34Wrapper.class, + U35Wrapper.class, + U36Wrapper.class, + U37Wrapper.class, + U38Wrapper.class, + U39Wrapper.class, + U40Wrapper.class, + U41Wrapper.class, + U42Wrapper.class, + U43Wrapper.class, + U44Wrapper.class, + U45Wrapper.class, + U46Wrapper.class, + U47Wrapper.class, + U48Wrapper.class, + U49Wrapper.class, + U50Wrapper.class, + U51Wrapper.class, + U52Wrapper.class, + U53Wrapper.class, + U54Wrapper.class, + U55Wrapper.class, + U56Wrapper.class, + U57Wrapper.class, + U58Wrapper.class, + U59Wrapper.class, + U60Wrapper.class, + U61Wrapper.class, + U62Wrapper.class, + U63Wrapper.class, + U64Wrapper.class, + U65Wrapper.class, + U66Wrapper.class, + U67Wrapper.class, + U68Wrapper.class, + U69Wrapper.class, + U70Wrapper.class, + U71Wrapper.class, + U72Wrapper.class, + U73Wrapper.class, + U74Wrapper.class, + U75Wrapper.class, + U76Wrapper.class, + U77Wrapper.class, + U78Wrapper.class, + U79Wrapper.class, + U80Wrapper.class, + U81Wrapper.class, + U82Wrapper.class, + U83Wrapper.class, + U84Wrapper.class, + U85Wrapper.class, + U86Wrapper.class, + U87Wrapper.class, + U88Wrapper.class, + U89Wrapper.class, + U90Wrapper.class, + U91Wrapper.class, + U92Wrapper.class, + U93Wrapper.class, + U94Wrapper.class, + U95Wrapper.class, + U96Wrapper.class, + U97Wrapper.class, + U98Wrapper.class, + U99Wrapper.class, + U100Wrapper.class + }; + + private volatile JsonDeserializer[] deserializers; + + @Override + public void resolve(DeserializationContext context) throws JsonMappingException { + deserializers = new JsonDeserializer[VARIANT_TYPES.length]; + } + + @Override + public boolean isCachable() { + return true; + } + + @Override + public LargeUnionExample deserialize(JsonParser parser, DeserializationContext context) throws IOException { + if (!parser.isExpectedStartObjectToken()) { + return context.reportInputMismatch( + LargeUnionExample.class, "Expected a JSON object for union deserialization"); + } + JsonToken firstToken = parser.nextToken(); + if (firstToken == JsonToken.FIELD_NAME && isTypeField(parser.currentName(), context)) { + if (parser.nextToken() != JsonToken.VALUE_STRING) { + return context.reportInputMismatch( + LargeUnionExample.class, "Union discriminator 'type' must be a string"); + } + String type = parser.getText(); + parser.nextToken(); + return deserializeSelected(parser, context, type); + } + return deserializeBuffered(parser, context); + } + + private LargeUnionExample deserializeBuffered(JsonParser parser, DeserializationContext context) + throws IOException { + try (TokenBuffer buffer = context.bufferForInputBuffering(parser)) { + buffer.writeStartObject(); + JsonToken token = parser.currentToken(); + while (token == JsonToken.FIELD_NAME) { + String fieldName = parser.currentName(); + JsonToken valueToken = parser.nextToken(); + if (isTypeField(fieldName, context)) { + if (valueToken != JsonToken.VALUE_STRING) { + return context.reportInputMismatch( + LargeUnionExample.class, "Union discriminator 'type' must be a string"); + } + String type = parser.getText(); + parser.nextToken(); + try (JsonParser bufferedParser = buffer.asParser(parser)) { + JsonParser combinedParser = + JsonParserSequence.createFlattened(true, bufferedParser, parser); + combinedParser.nextToken(); + return deserializeSelected(combinedParser, context, type); + } + } + buffer.writeFieldName(fieldName); + buffer.copyCurrentStructure(parser); + token = parser.nextToken(); + } + if (token != JsonToken.END_OBJECT) { + return context.reportInputMismatch( + LargeUnionExample.class, "Expected the end of a JSON object while deserializing a union"); + } + } + return context.reportInputMismatch(LargeUnionExample.class, "Union discriminator 'type' is required"); + } + + private static boolean isTypeField(String fieldName, DeserializationContext context) { + return "type".equals(fieldName) + || (context.isEnabled(MapperFeature.ACCEPT_CASE_INSENSITIVE_PROPERTIES) + && "type".equalsIgnoreCase(fieldName)); + } + + private LargeUnionExample deserializeSelected(JsonParser parser, DeserializationContext context, String type) + throws IOException { + int variantIndex = + switch (type) { + case "u0" -> 0; + case "u1" -> 1; + case "u2" -> 2; + case "u3" -> 3; + case "u4" -> 4; + case "u5" -> 5; + case "u6" -> 6; + case "u7" -> 7; + case "u8" -> 8; + case "u9" -> 9; + case "u10" -> 10; + case "u11" -> 11; + case "u12" -> 12; + case "u13" -> 13; + case "u14" -> 14; + case "u15" -> 15; + case "u16" -> 16; + case "u17" -> 17; + case "u18" -> 18; + case "u19" -> 19; + case "u20" -> 20; + case "u21" -> 21; + case "u22" -> 22; + case "u23" -> 23; + case "u24" -> 24; + case "u25" -> 25; + case "u26" -> 26; + case "u27" -> 27; + case "u28" -> 28; + case "u29" -> 29; + case "u30" -> 30; + case "u31" -> 31; + case "u32" -> 32; + case "u33" -> 33; + case "u34" -> 34; + case "u35" -> 35; + case "u36" -> 36; + case "u37" -> 37; + case "u38" -> 38; + case "u39" -> 39; + case "u40" -> 40; + case "u41" -> 41; + case "u42" -> 42; + case "u43" -> 43; + case "u44" -> 44; + case "u45" -> 45; + case "u46" -> 46; + case "u47" -> 47; + case "u48" -> 48; + case "u49" -> 49; + case "u50" -> 50; + case "u51" -> 51; + case "u52" -> 52; + case "u53" -> 53; + case "u54" -> 54; + case "u55" -> 55; + case "u56" -> 56; + case "u57" -> 57; + case "u58" -> 58; + case "u59" -> 59; + case "u60" -> 60; + case "u61" -> 61; + case "u62" -> 62; + case "u63" -> 63; + case "u64" -> 64; + case "u65" -> 65; + case "u66" -> 66; + case "u67" -> 67; + case "u68" -> 68; + case "u69" -> 69; + case "u70" -> 70; + case "u71" -> 71; + case "u72" -> 72; + case "u73" -> 73; + case "u74" -> 74; + case "u75" -> 75; + case "u76" -> 76; + case "u77" -> 77; + case "u78" -> 78; + case "u79" -> 79; + case "u80" -> 80; + case "u81" -> 81; + case "u82" -> 82; + case "u83" -> 83; + case "u84" -> 84; + case "u85" -> 85; + case "u86" -> 86; + case "u87" -> 87; + case "u88" -> 88; + case "u89" -> 89; + case "u90" -> 90; + case "u91" -> 91; + case "u92" -> 92; + case "u93" -> 93; + case "u94" -> 94; + case "u95" -> 95; + case "u96" -> 96; + case "u97" -> 97; + case "u98" -> 98; + case "u99" -> 99; + case "u100" -> 100; + default -> -1; + }; + if (variantIndex < 0) { + return deserializeUnknown(parser, context, type); + } + JsonDeserializer deserializer = deserializers[variantIndex]; + if (deserializer == null) { + deserializer = resolveDeserializer(context, variantIndex); + } + return new LargeUnionExample((Base) deserializer.deserialize(parser, context)); + } + + private synchronized JsonDeserializer resolveDeserializer(DeserializationContext context, int variantIndex) + throws JsonMappingException { + JsonDeserializer deserializer = deserializers[variantIndex]; + if (deserializer == null) { + deserializer = context.findRootValueDeserializer(context.constructType(VARIANT_TYPES[variantIndex])); + JsonDeserializer[] updated = deserializers.clone(); + updated[variantIndex] = deserializer; + deserializers = updated; + } + return deserializer; + } + + private static LargeUnionExample deserializeUnknown( + JsonParser parser, DeserializationContext context, String type) throws IOException { + Map values = new HashMap<>(); + if (parser.currentToken() == JsonToken.START_OBJECT) { + parser.nextToken(); + } + while (parser.currentToken() == JsonToken.FIELD_NAME) { + String fieldName = parser.currentName(); + parser.nextToken(); + values.put(fieldName, context.readValue(parser, Object.class)); + parser.nextToken(); + } + if (parser.currentToken() != JsonToken.END_OBJECT) { + return context.reportInputMismatch( + LargeUnionExample.class, "Expected the end of a JSON object while deserializing a union"); + } + return new LargeUnionExample(new UnknownWrapper(type, values)); + } + } } diff --git a/conjure-java-core/src/integrationInput/java/template/com/palantir/product/ManyFieldExample.java b/conjure-java-core/src/integrationInput/java/template/com/palantir/product/ManyFieldExample.java index c17179657..782b04f5a 100644 --- a/conjure-java-core/src/integrationInput/java/template/com/palantir/product/ManyFieldExample.java +++ b/conjure-java-core/src/integrationInput/java/template/com/palantir/product/ManyFieldExample.java @@ -372,6 +372,7 @@ static final class DefaultBuilder implements Builder { private Set set = ConjureCollections.newNonNullSet(); + @JsonSetter(value = "map", nulls = Nulls.SKIP, contentNulls = Nulls.FAIL) private Map map = new LinkedHashMap<>(); private StringAliasExample alias; @@ -508,7 +509,6 @@ public Builder set(String set) { /** @deprecated deprecation documentation. */ @Deprecated @Override - @JsonSetter(value = "map", nulls = Nulls.SKIP, contentNulls = Nulls.FAIL) public Builder map(@Nonnull Map map) { checkNotBuilt(); this.map = new LinkedHashMap<>(Preconditions.checkNotNull(map, "map cannot be null")); diff --git a/conjure-java-core/src/integrationInput/java/template/com/palantir/product/MapExample.java b/conjure-java-core/src/integrationInput/java/template/com/palantir/product/MapExample.java index cb6c744a1..c854d2ce2 100644 --- a/conjure-java-core/src/integrationInput/java/template/com/palantir/product/MapExample.java +++ b/conjure-java-core/src/integrationInput/java/template/com/palantir/product/MapExample.java @@ -123,10 +123,13 @@ public static Builder builder() { public static final class Builder { boolean _buildInvoked; + @JsonSetter(value = "items", nulls = Nulls.SKIP, contentNulls = Nulls.FAIL) private Map items = new LinkedHashMap<>(); + @JsonSetter(value = "optionalItems", nulls = Nulls.SKIP, contentNulls = Nulls.AS_EMPTY) private Map> optionalItems = new LinkedHashMap<>(); + @JsonSetter(value = "aliasOptionalItems", nulls = Nulls.SKIP, contentNulls = Nulls.AS_EMPTY) private Map aliasOptionalItems = new LinkedHashMap<>(); private Builder() {} @@ -139,7 +142,6 @@ public Builder from(MapExample other) { return this; } - @JsonSetter(value = "items", nulls = Nulls.SKIP, contentNulls = Nulls.FAIL) public Builder items(@Nonnull Map items) { checkNotBuilt(); this.items = new LinkedHashMap<>(Preconditions.checkNotNull(items, "items cannot be null")); @@ -158,7 +160,6 @@ public Builder items(String key, String value) { return this; } - @JsonSetter(value = "optionalItems", nulls = Nulls.SKIP, contentNulls = Nulls.AS_EMPTY) public Builder optionalItems(@Nonnull Map> optionalItems) { checkNotBuilt(); this.optionalItems = @@ -178,7 +179,6 @@ public Builder optionalItems(String key, Optional value) { return this; } - @JsonSetter(value = "aliasOptionalItems", nulls = Nulls.SKIP, contentNulls = Nulls.AS_EMPTY) public Builder aliasOptionalItems(@Nonnull Map aliasOptionalItems) { checkNotBuilt(); this.aliasOptionalItems = new LinkedHashMap<>( diff --git a/conjure-java-core/src/integrationInput/java/template/com/palantir/product/SingleUnion.java b/conjure-java-core/src/integrationInput/java/template/com/palantir/product/SingleUnion.java index 7ad4d3f80..f464f59a8 100644 --- a/conjure-java-core/src/integrationInput/java/template/com/palantir/product/SingleUnion.java +++ b/conjure-java-core/src/integrationInput/java/template/com/palantir/product/SingleUnion.java @@ -6,14 +6,23 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.annotation.JsonSetter; -import com.fasterxml.jackson.annotation.JsonSubTypes; -import com.fasterxml.jackson.annotation.JsonTypeInfo; import com.fasterxml.jackson.annotation.JsonTypeName; import com.fasterxml.jackson.annotation.JsonValue; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.JsonToken; +import com.fasterxml.jackson.core.util.JsonParserSequence; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonDeserializer; +import com.fasterxml.jackson.databind.JsonMappingException; +import com.fasterxml.jackson.databind.MapperFeature; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.deser.ResolvableDeserializer; +import com.fasterxml.jackson.databind.util.TokenBuffer; import com.palantir.logsafe.Preconditions; import com.palantir.logsafe.Safe; import com.palantir.logsafe.SafeArg; import com.palantir.logsafe.exceptions.SafeIllegalArgumentException; +import java.io.IOException; import java.util.Collections; import java.util.HashMap; import java.util.Map; @@ -23,6 +32,7 @@ import javax.annotation.processing.Generated; @Generated("com.palantir.conjure.java.types.UnionGenerator") +@JsonDeserialize(using = SingleUnion.Deserializer.class) public final class SingleUnion { private final Base value; @@ -144,19 +154,12 @@ public interface Completed_StageVisitorBuilder { Visitor build(); } - @JsonTypeInfo( - use = JsonTypeInfo.Id.NAME, - include = JsonTypeInfo.As.EXISTING_PROPERTY, - property = "type", - visible = true, - defaultImpl = UnknownWrapper.class) - @JsonSubTypes(@JsonSubTypes.Type(FooWrapper.class)) - @JsonIgnoreProperties(ignoreUnknown = true) private interface Base { T accept(Visitor visitor); } @JsonTypeName("foo") + @JsonIgnoreProperties(ignoreUnknown = true) private static final class FooWrapper implements Base { private final String value; @@ -260,4 +263,126 @@ public String toString() { return "UnknownWrapper{type: " + type + ", value: " + value + '}'; } } + + static final class Deserializer extends JsonDeserializer implements ResolvableDeserializer { + private static final Class[] VARIANT_TYPES = new Class[] {FooWrapper.class}; + + private volatile JsonDeserializer[] deserializers; + + @Override + public void resolve(DeserializationContext context) throws JsonMappingException { + deserializers = new JsonDeserializer[VARIANT_TYPES.length]; + } + + @Override + public boolean isCachable() { + return true; + } + + @Override + public SingleUnion deserialize(JsonParser parser, DeserializationContext context) throws IOException { + if (!parser.isExpectedStartObjectToken()) { + return context.reportInputMismatch( + SingleUnion.class, "Expected a JSON object for union deserialization"); + } + JsonToken firstToken = parser.nextToken(); + if (firstToken == JsonToken.FIELD_NAME && isTypeField(parser.currentName(), context)) { + if (parser.nextToken() != JsonToken.VALUE_STRING) { + return context.reportInputMismatch( + SingleUnion.class, "Union discriminator 'type' must be a string"); + } + String type = parser.getText(); + parser.nextToken(); + return deserializeSelected(parser, context, type); + } + return deserializeBuffered(parser, context); + } + + private SingleUnion deserializeBuffered(JsonParser parser, DeserializationContext context) throws IOException { + try (TokenBuffer buffer = context.bufferForInputBuffering(parser)) { + buffer.writeStartObject(); + JsonToken token = parser.currentToken(); + while (token == JsonToken.FIELD_NAME) { + String fieldName = parser.currentName(); + JsonToken valueToken = parser.nextToken(); + if (isTypeField(fieldName, context)) { + if (valueToken != JsonToken.VALUE_STRING) { + return context.reportInputMismatch( + SingleUnion.class, "Union discriminator 'type' must be a string"); + } + String type = parser.getText(); + parser.nextToken(); + try (JsonParser bufferedParser = buffer.asParser(parser)) { + JsonParser combinedParser = + JsonParserSequence.createFlattened(true, bufferedParser, parser); + combinedParser.nextToken(); + return deserializeSelected(combinedParser, context, type); + } + } + buffer.writeFieldName(fieldName); + buffer.copyCurrentStructure(parser); + token = parser.nextToken(); + } + if (token != JsonToken.END_OBJECT) { + return context.reportInputMismatch( + SingleUnion.class, "Expected the end of a JSON object while deserializing a union"); + } + } + return context.reportInputMismatch(SingleUnion.class, "Union discriminator 'type' is required"); + } + + private static boolean isTypeField(String fieldName, DeserializationContext context) { + return "type".equals(fieldName) + || (context.isEnabled(MapperFeature.ACCEPT_CASE_INSENSITIVE_PROPERTIES) + && "type".equalsIgnoreCase(fieldName)); + } + + private SingleUnion deserializeSelected(JsonParser parser, DeserializationContext context, String type) + throws IOException { + int variantIndex = + switch (type) { + case "foo" -> 0; + default -> -1; + }; + if (variantIndex < 0) { + return deserializeUnknown(parser, context, type); + } + JsonDeserializer deserializer = deserializers[variantIndex]; + if (deserializer == null) { + deserializer = resolveDeserializer(context, variantIndex); + } + return new SingleUnion((Base) deserializer.deserialize(parser, context)); + } + + private synchronized JsonDeserializer resolveDeserializer(DeserializationContext context, int variantIndex) + throws JsonMappingException { + JsonDeserializer deserializer = deserializers[variantIndex]; + if (deserializer == null) { + deserializer = context.findRootValueDeserializer(context.constructType(VARIANT_TYPES[variantIndex])); + JsonDeserializer[] updated = deserializers.clone(); + updated[variantIndex] = deserializer; + deserializers = updated; + } + return deserializer; + } + + private static SingleUnion deserializeUnknown(JsonParser parser, DeserializationContext context, String type) + throws IOException { + Map values = new HashMap<>(); + if (parser.currentToken() == JsonToken.START_OBJECT) { + parser.nextToken(); + } + while (parser.currentToken() == JsonToken.FIELD_NAME) { + String fieldName = parser.currentName(); + parser.nextToken(); + values.put(fieldName, context.readValue(parser, Object.class)); + parser.nextToken(); + } + if (parser.currentToken() != JsonToken.END_OBJECT) { + return context.reportInputMismatch( + SingleUnion.class, "Expected the end of a JSON object while deserializing a union"); + } + return new SingleUnion(new UnknownWrapper(type, values)); + } + } } diff --git a/conjure-java-core/src/integrationInput/java/template/com/palantir/product/Union.java b/conjure-java-core/src/integrationInput/java/template/com/palantir/product/Union.java index 00da4ffc7..215a0b8f3 100644 --- a/conjure-java-core/src/integrationInput/java/template/com/palantir/product/Union.java +++ b/conjure-java-core/src/integrationInput/java/template/com/palantir/product/Union.java @@ -6,14 +6,23 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.annotation.JsonSetter; -import com.fasterxml.jackson.annotation.JsonSubTypes; -import com.fasterxml.jackson.annotation.JsonTypeInfo; import com.fasterxml.jackson.annotation.JsonTypeName; import com.fasterxml.jackson.annotation.JsonValue; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.JsonToken; +import com.fasterxml.jackson.core.util.JsonParserSequence; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonDeserializer; +import com.fasterxml.jackson.databind.JsonMappingException; +import com.fasterxml.jackson.databind.MapperFeature; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.deser.ResolvableDeserializer; +import com.fasterxml.jackson.databind.util.TokenBuffer; import com.palantir.logsafe.Preconditions; import com.palantir.logsafe.Safe; import com.palantir.logsafe.SafeArg; import com.palantir.logsafe.exceptions.SafeIllegalArgumentException; +import java.io.IOException; import java.util.Collections; import java.util.HashMap; import java.util.Map; @@ -24,6 +33,7 @@ import javax.annotation.processing.Generated; @Generated("com.palantir.conjure.java.types.UnionGenerator") +@JsonDeserialize(using = Union.Deserializer.class) public final class Union { private final Base value; @@ -221,23 +231,12 @@ public interface Completed_StageVisitorBuilder { Visitor build(); } - @JsonTypeInfo( - use = JsonTypeInfo.Id.NAME, - include = JsonTypeInfo.As.EXISTING_PROPERTY, - property = "type", - visible = true, - defaultImpl = UnknownWrapper.class) - @JsonSubTypes({ - @JsonSubTypes.Type(FooWrapper.class), - @JsonSubTypes.Type(BarWrapper.class), - @JsonSubTypes.Type(BazWrapper.class) - }) - @JsonIgnoreProperties(ignoreUnknown = true) private interface Base { T accept(Visitor visitor); } @JsonTypeName("foo") + @JsonIgnoreProperties(ignoreUnknown = true) private static final class FooWrapper implements Base { private final String value; @@ -283,6 +282,7 @@ public String toString() { } @JsonTypeName("bar") + @JsonIgnoreProperties(ignoreUnknown = true) private static final class BarWrapper implements Base { private final int value; @@ -329,6 +329,7 @@ public String toString() { } @JsonTypeName("baz") + @JsonIgnoreProperties(ignoreUnknown = true) private static final class BazWrapper implements Base { private final long value; @@ -433,4 +434,127 @@ public String toString() { return "UnknownWrapper{type: " + type + ", value: " + value + '}'; } } + + static final class Deserializer extends JsonDeserializer implements ResolvableDeserializer { + private static final Class[] VARIANT_TYPES = + new Class[] {FooWrapper.class, BarWrapper.class, BazWrapper.class}; + + private volatile JsonDeserializer[] deserializers; + + @Override + public void resolve(DeserializationContext context) throws JsonMappingException { + deserializers = new JsonDeserializer[VARIANT_TYPES.length]; + } + + @Override + public boolean isCachable() { + return true; + } + + @Override + public Union deserialize(JsonParser parser, DeserializationContext context) throws IOException { + if (!parser.isExpectedStartObjectToken()) { + return context.reportInputMismatch(Union.class, "Expected a JSON object for union deserialization"); + } + JsonToken firstToken = parser.nextToken(); + if (firstToken == JsonToken.FIELD_NAME && isTypeField(parser.currentName(), context)) { + if (parser.nextToken() != JsonToken.VALUE_STRING) { + return context.reportInputMismatch(Union.class, "Union discriminator 'type' must be a string"); + } + String type = parser.getText(); + parser.nextToken(); + return deserializeSelected(parser, context, type); + } + return deserializeBuffered(parser, context); + } + + private Union deserializeBuffered(JsonParser parser, DeserializationContext context) throws IOException { + try (TokenBuffer buffer = context.bufferForInputBuffering(parser)) { + buffer.writeStartObject(); + JsonToken token = parser.currentToken(); + while (token == JsonToken.FIELD_NAME) { + String fieldName = parser.currentName(); + JsonToken valueToken = parser.nextToken(); + if (isTypeField(fieldName, context)) { + if (valueToken != JsonToken.VALUE_STRING) { + return context.reportInputMismatch( + Union.class, "Union discriminator 'type' must be a string"); + } + String type = parser.getText(); + parser.nextToken(); + try (JsonParser bufferedParser = buffer.asParser(parser)) { + JsonParser combinedParser = + JsonParserSequence.createFlattened(true, bufferedParser, parser); + combinedParser.nextToken(); + return deserializeSelected(combinedParser, context, type); + } + } + buffer.writeFieldName(fieldName); + buffer.copyCurrentStructure(parser); + token = parser.nextToken(); + } + if (token != JsonToken.END_OBJECT) { + return context.reportInputMismatch( + Union.class, "Expected the end of a JSON object while deserializing a union"); + } + } + return context.reportInputMismatch(Union.class, "Union discriminator 'type' is required"); + } + + private static boolean isTypeField(String fieldName, DeserializationContext context) { + return "type".equals(fieldName) + || (context.isEnabled(MapperFeature.ACCEPT_CASE_INSENSITIVE_PROPERTIES) + && "type".equalsIgnoreCase(fieldName)); + } + + private Union deserializeSelected(JsonParser parser, DeserializationContext context, String type) + throws IOException { + int variantIndex = + switch (type) { + case "foo" -> 0; + case "bar" -> 1; + case "baz" -> 2; + default -> -1; + }; + if (variantIndex < 0) { + return deserializeUnknown(parser, context, type); + } + JsonDeserializer deserializer = deserializers[variantIndex]; + if (deserializer == null) { + deserializer = resolveDeserializer(context, variantIndex); + } + return new Union((Base) deserializer.deserialize(parser, context)); + } + + private synchronized JsonDeserializer resolveDeserializer(DeserializationContext context, int variantIndex) + throws JsonMappingException { + JsonDeserializer deserializer = deserializers[variantIndex]; + if (deserializer == null) { + deserializer = context.findRootValueDeserializer(context.constructType(VARIANT_TYPES[variantIndex])); + JsonDeserializer[] updated = deserializers.clone(); + updated[variantIndex] = deserializer; + deserializers = updated; + } + return deserializer; + } + + private static Union deserializeUnknown(JsonParser parser, DeserializationContext context, String type) + throws IOException { + Map values = new HashMap<>(); + if (parser.currentToken() == JsonToken.START_OBJECT) { + parser.nextToken(); + } + while (parser.currentToken() == JsonToken.FIELD_NAME) { + String fieldName = parser.currentName(); + parser.nextToken(); + values.put(fieldName, context.readValue(parser, Object.class)); + parser.nextToken(); + } + if (parser.currentToken() != JsonToken.END_OBJECT) { + return context.reportInputMismatch( + Union.class, "Expected the end of a JSON object while deserializing a union"); + } + return new Union(new UnknownWrapper(type, values)); + } + } } diff --git a/conjure-java-core/src/integrationInput/java/template/com/palantir/product/UnionTypeExample.java b/conjure-java-core/src/integrationInput/java/template/com/palantir/product/UnionTypeExample.java index e26284b3b..4aaac64ac 100644 --- a/conjure-java-core/src/integrationInput/java/template/com/palantir/product/UnionTypeExample.java +++ b/conjure-java-core/src/integrationInput/java/template/com/palantir/product/UnionTypeExample.java @@ -6,17 +6,25 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.annotation.JsonSetter; -import com.fasterxml.jackson.annotation.JsonSubTypes; -import com.fasterxml.jackson.annotation.JsonTypeInfo; import com.fasterxml.jackson.annotation.JsonTypeName; import com.fasterxml.jackson.annotation.JsonValue; import com.fasterxml.jackson.annotation.Nulls; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.JsonToken; +import com.fasterxml.jackson.core.util.JsonParserSequence; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonDeserializer; +import com.fasterxml.jackson.databind.JsonMappingException; +import com.fasterxml.jackson.databind.MapperFeature; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.deser.ResolvableDeserializer; +import com.fasterxml.jackson.databind.util.TokenBuffer; import com.palantir.logsafe.Preconditions; import com.palantir.logsafe.Safe; import com.palantir.logsafe.SafeArg; import com.palantir.logsafe.Unsafe; import com.palantir.logsafe.exceptions.SafeIllegalArgumentException; +import java.io.IOException; import java.util.Collections; import java.util.HashMap; import java.util.LinkedHashSet; @@ -34,6 +42,7 @@ /** A type which can either be a StringExample, a set of strings, or an integer. */ @Unsafe @Generated("com.palantir.conjure.java.types.UnionGenerator") +@JsonDeserialize(using = UnionTypeExample.Deserializer.class) public final class UnionTypeExample { private final Base value; @@ -685,39 +694,12 @@ public interface Completed_StageVisitorBuilder { Visitor build(); } - @JsonTypeInfo( - use = JsonTypeInfo.Id.NAME, - include = JsonTypeInfo.As.EXISTING_PROPERTY, - property = "type", - visible = true, - defaultImpl = UnknownWrapper.class) - @JsonSubTypes({ - @JsonSubTypes.Type(StringExampleWrapper.class), - @JsonSubTypes.Type(ThisFieldIsAnIntegerWrapper.class), - @JsonSubTypes.Type(AlsoAnIntegerWrapper.class), - @JsonSubTypes.Type(IfWrapper.class), - @JsonSubTypes.Type(NewWrapper.class), - @JsonSubTypes.Type(InterfaceWrapper.class), - @JsonSubTypes.Type(CompletedWrapper.class), - @JsonSubTypes.Type(Unknown_Wrapper.class), - @JsonSubTypes.Type(OptionalWrapper.class), - @JsonSubTypes.Type(ListWrapper.class), - @JsonSubTypes.Type(SetWrapper.class), - @JsonSubTypes.Type(MapWrapper.class), - @JsonSubTypes.Type(OptionalAliasWrapper.class), - @JsonSubTypes.Type(ListAliasWrapper.class), - @JsonSubTypes.Type(SetAliasWrapper.class), - @JsonSubTypes.Type(MapAliasWrapper.class), - @JsonSubTypes.Type(BooleanFieldWrapper.class), - @JsonSubTypes.Type(SafeIntWrapper.class), - @JsonSubTypes.Type(UnsafeDoubleWrapper.class) - }) - @JsonIgnoreProperties(ignoreUnknown = true) private interface Base { T accept(Visitor visitor); } @JsonTypeName("stringExample") + @JsonIgnoreProperties(ignoreUnknown = true) private static final class StringExampleWrapper implements Base { private final StringExample value; @@ -763,6 +745,7 @@ public String toString() { } @JsonTypeName("thisFieldIsAnInteger") + @JsonIgnoreProperties(ignoreUnknown = true) private static final class ThisFieldIsAnIntegerWrapper implements Base { private final int value; @@ -809,6 +792,7 @@ public String toString() { } @JsonTypeName("alsoAnInteger") + @JsonIgnoreProperties(ignoreUnknown = true) private static final class AlsoAnIntegerWrapper implements Base { private final int value; @@ -854,6 +838,7 @@ public String toString() { } @JsonTypeName("if") + @JsonIgnoreProperties(ignoreUnknown = true) private static final class IfWrapper implements Base { private final int value; @@ -899,6 +884,7 @@ public String toString() { } @JsonTypeName("new") + @JsonIgnoreProperties(ignoreUnknown = true) private static final class NewWrapper implements Base { private final int value; @@ -944,6 +930,7 @@ public String toString() { } @JsonTypeName("interface") + @JsonIgnoreProperties(ignoreUnknown = true) private static final class InterfaceWrapper implements Base { private final int value; @@ -989,6 +976,7 @@ public String toString() { } @JsonTypeName("completed") + @JsonIgnoreProperties(ignoreUnknown = true) private static final class CompletedWrapper implements Base { private final int value; @@ -1034,6 +1022,7 @@ public String toString() { } @JsonTypeName("unknown") + @JsonIgnoreProperties(ignoreUnknown = true) private static final class Unknown_Wrapper implements Base { private final int value; @@ -1079,6 +1068,7 @@ public String toString() { } @JsonTypeName("optional") + @JsonIgnoreProperties(ignoreUnknown = true) private static final class OptionalWrapper implements Base { private final Optional value; @@ -1125,6 +1115,7 @@ public String toString() { } @JsonTypeName("list") + @JsonIgnoreProperties(ignoreUnknown = true) private static final class ListWrapper implements Base { private final List value; @@ -1170,6 +1161,7 @@ public String toString() { } @JsonTypeName("set") + @JsonIgnoreProperties(ignoreUnknown = true) private static final class SetWrapper implements Base { private final Set value; @@ -1217,6 +1209,7 @@ public String toString() { } @JsonTypeName("map") + @JsonIgnoreProperties(ignoreUnknown = true) private static final class MapWrapper implements Base { private final Map value; @@ -1262,6 +1255,7 @@ public String toString() { } @JsonTypeName("optionalAlias") + @JsonIgnoreProperties(ignoreUnknown = true) private static final class OptionalAliasWrapper implements Base { private final OptionalAlias value; @@ -1308,6 +1302,7 @@ public String toString() { } @JsonTypeName("listAlias") + @JsonIgnoreProperties(ignoreUnknown = true) private static final class ListAliasWrapper implements Base { private final ListAlias value; @@ -1353,6 +1348,7 @@ public String toString() { } @JsonTypeName("setAlias") + @JsonIgnoreProperties(ignoreUnknown = true) private static final class SetAliasWrapper implements Base { private final SetAlias value; @@ -1398,6 +1394,7 @@ public String toString() { } @JsonTypeName("mapAlias") + @JsonIgnoreProperties(ignoreUnknown = true) private static final class MapAliasWrapper implements Base { private final MapAliasExample value; @@ -1444,6 +1441,7 @@ public String toString() { } @JsonTypeName("booleanField") + @JsonIgnoreProperties(ignoreUnknown = true) private static final class BooleanFieldWrapper implements Base { private final boolean value; @@ -1489,6 +1487,7 @@ public String toString() { } @JsonTypeName("safeInt") + @JsonIgnoreProperties(ignoreUnknown = true) private static final class SafeIntWrapper implements Base { private final int value; @@ -1534,6 +1533,7 @@ public String toString() { } @JsonTypeName("unsafeDouble") + @JsonIgnoreProperties(ignoreUnknown = true) private static final class UnsafeDoubleWrapper implements Base { private final double value; @@ -1637,4 +1637,165 @@ public String toString() { return "UnknownWrapper{type: " + type + ", value: " + value + '}'; } } + + static final class Deserializer extends JsonDeserializer implements ResolvableDeserializer { + private static final Class[] VARIANT_TYPES = new Class[] { + StringExampleWrapper.class, + ThisFieldIsAnIntegerWrapper.class, + AlsoAnIntegerWrapper.class, + IfWrapper.class, + NewWrapper.class, + InterfaceWrapper.class, + CompletedWrapper.class, + Unknown_Wrapper.class, + OptionalWrapper.class, + ListWrapper.class, + SetWrapper.class, + MapWrapper.class, + OptionalAliasWrapper.class, + ListAliasWrapper.class, + SetAliasWrapper.class, + MapAliasWrapper.class, + BooleanFieldWrapper.class, + SafeIntWrapper.class, + UnsafeDoubleWrapper.class + }; + + private volatile JsonDeserializer[] deserializers; + + @Override + public void resolve(DeserializationContext context) throws JsonMappingException { + deserializers = new JsonDeserializer[VARIANT_TYPES.length]; + } + + @Override + public boolean isCachable() { + return true; + } + + @Override + public UnionTypeExample deserialize(JsonParser parser, DeserializationContext context) throws IOException { + if (!parser.isExpectedStartObjectToken()) { + return context.reportInputMismatch( + UnionTypeExample.class, "Expected a JSON object for union deserialization"); + } + JsonToken firstToken = parser.nextToken(); + if (firstToken == JsonToken.FIELD_NAME && isTypeField(parser.currentName(), context)) { + if (parser.nextToken() != JsonToken.VALUE_STRING) { + return context.reportInputMismatch( + UnionTypeExample.class, "Union discriminator 'type' must be a string"); + } + String type = parser.getText(); + parser.nextToken(); + return deserializeSelected(parser, context, type); + } + return deserializeBuffered(parser, context); + } + + private UnionTypeExample deserializeBuffered(JsonParser parser, DeserializationContext context) + throws IOException { + try (TokenBuffer buffer = context.bufferForInputBuffering(parser)) { + buffer.writeStartObject(); + JsonToken token = parser.currentToken(); + while (token == JsonToken.FIELD_NAME) { + String fieldName = parser.currentName(); + JsonToken valueToken = parser.nextToken(); + if (isTypeField(fieldName, context)) { + if (valueToken != JsonToken.VALUE_STRING) { + return context.reportInputMismatch( + UnionTypeExample.class, "Union discriminator 'type' must be a string"); + } + String type = parser.getText(); + parser.nextToken(); + try (JsonParser bufferedParser = buffer.asParser(parser)) { + JsonParser combinedParser = + JsonParserSequence.createFlattened(true, bufferedParser, parser); + combinedParser.nextToken(); + return deserializeSelected(combinedParser, context, type); + } + } + buffer.writeFieldName(fieldName); + buffer.copyCurrentStructure(parser); + token = parser.nextToken(); + } + if (token != JsonToken.END_OBJECT) { + return context.reportInputMismatch( + UnionTypeExample.class, "Expected the end of a JSON object while deserializing a union"); + } + } + return context.reportInputMismatch(UnionTypeExample.class, "Union discriminator 'type' is required"); + } + + private static boolean isTypeField(String fieldName, DeserializationContext context) { + return "type".equals(fieldName) + || (context.isEnabled(MapperFeature.ACCEPT_CASE_INSENSITIVE_PROPERTIES) + && "type".equalsIgnoreCase(fieldName)); + } + + private UnionTypeExample deserializeSelected(JsonParser parser, DeserializationContext context, String type) + throws IOException { + int variantIndex = + switch (type) { + case "stringExample" -> 0; + case "thisFieldIsAnInteger" -> 1; + case "alsoAnInteger" -> 2; + case "if" -> 3; + case "new" -> 4; + case "interface" -> 5; + case "completed" -> 6; + case "unknown" -> 7; + case "optional" -> 8; + case "list" -> 9; + case "set" -> 10; + case "map" -> 11; + case "optionalAlias" -> 12; + case "listAlias" -> 13; + case "setAlias" -> 14; + case "mapAlias" -> 15; + case "booleanField" -> 16; + case "safeInt" -> 17; + case "unsafeDouble" -> 18; + default -> -1; + }; + if (variantIndex < 0) { + return deserializeUnknown(parser, context, type); + } + JsonDeserializer deserializer = deserializers[variantIndex]; + if (deserializer == null) { + deserializer = resolveDeserializer(context, variantIndex); + } + return new UnionTypeExample((Base) deserializer.deserialize(parser, context)); + } + + private synchronized JsonDeserializer resolveDeserializer(DeserializationContext context, int variantIndex) + throws JsonMappingException { + JsonDeserializer deserializer = deserializers[variantIndex]; + if (deserializer == null) { + deserializer = context.findRootValueDeserializer(context.constructType(VARIANT_TYPES[variantIndex])); + JsonDeserializer[] updated = deserializers.clone(); + updated[variantIndex] = deserializer; + deserializers = updated; + } + return deserializer; + } + + private static UnionTypeExample deserializeUnknown( + JsonParser parser, DeserializationContext context, String type) throws IOException { + Map values = new HashMap<>(); + if (parser.currentToken() == JsonToken.START_OBJECT) { + parser.nextToken(); + } + while (parser.currentToken() == JsonToken.FIELD_NAME) { + String fieldName = parser.currentName(); + parser.nextToken(); + values.put(fieldName, context.readValue(parser, Object.class)); + parser.nextToken(); + } + if (parser.currentToken() != JsonToken.END_OBJECT) { + return context.reportInputMismatch( + UnionTypeExample.class, "Expected the end of a JSON object while deserializing a union"); + } + return new UnionTypeExample(new UnknownWrapper(type, values)); + } + } } diff --git a/conjure-java-core/src/integrationInput/java/template/com/palantir/product/UnionWithUnknownString.java b/conjure-java-core/src/integrationInput/java/template/com/palantir/product/UnionWithUnknownString.java index e6c83d3db..764a2c5e4 100644 --- a/conjure-java-core/src/integrationInput/java/template/com/palantir/product/UnionWithUnknownString.java +++ b/conjure-java-core/src/integrationInput/java/template/com/palantir/product/UnionWithUnknownString.java @@ -6,14 +6,23 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.annotation.JsonSetter; -import com.fasterxml.jackson.annotation.JsonSubTypes; -import com.fasterxml.jackson.annotation.JsonTypeInfo; import com.fasterxml.jackson.annotation.JsonTypeName; import com.fasterxml.jackson.annotation.JsonValue; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.JsonToken; +import com.fasterxml.jackson.core.util.JsonParserSequence; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonDeserializer; +import com.fasterxml.jackson.databind.JsonMappingException; +import com.fasterxml.jackson.databind.MapperFeature; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.deser.ResolvableDeserializer; +import com.fasterxml.jackson.databind.util.TokenBuffer; import com.palantir.logsafe.Preconditions; import com.palantir.logsafe.Safe; import com.palantir.logsafe.SafeArg; import com.palantir.logsafe.exceptions.SafeIllegalArgumentException; +import java.io.IOException; import java.util.Collections; import java.util.HashMap; import java.util.Map; @@ -23,6 +32,7 @@ import javax.annotation.processing.Generated; @Generated("com.palantir.conjure.java.types.UnionGenerator") +@JsonDeserialize(using = UnionWithUnknownString.Deserializer.class) public final class UnionWithUnknownString { private final Base value; @@ -145,19 +155,12 @@ public interface Completed_StageVisitorBuilder { Visitor build(); } - @JsonTypeInfo( - use = JsonTypeInfo.Id.NAME, - include = JsonTypeInfo.As.EXISTING_PROPERTY, - property = "type", - visible = true, - defaultImpl = UnknownWrapper.class) - @JsonSubTypes(@JsonSubTypes.Type(Unknown_Wrapper.class)) - @JsonIgnoreProperties(ignoreUnknown = true) private interface Base { T accept(Visitor visitor); } @JsonTypeName("unknown") + @JsonIgnoreProperties(ignoreUnknown = true) private static final class Unknown_Wrapper implements Base { private final String value; @@ -261,4 +264,129 @@ public String toString() { return "UnknownWrapper{type: " + type + ", value: " + value + '}'; } } + + static final class Deserializer extends JsonDeserializer implements ResolvableDeserializer { + private static final Class[] VARIANT_TYPES = new Class[] {Unknown_Wrapper.class}; + + private volatile JsonDeserializer[] deserializers; + + @Override + public void resolve(DeserializationContext context) throws JsonMappingException { + deserializers = new JsonDeserializer[VARIANT_TYPES.length]; + } + + @Override + public boolean isCachable() { + return true; + } + + @Override + public UnionWithUnknownString deserialize(JsonParser parser, DeserializationContext context) + throws IOException { + if (!parser.isExpectedStartObjectToken()) { + return context.reportInputMismatch( + UnionWithUnknownString.class, "Expected a JSON object for union deserialization"); + } + JsonToken firstToken = parser.nextToken(); + if (firstToken == JsonToken.FIELD_NAME && isTypeField(parser.currentName(), context)) { + if (parser.nextToken() != JsonToken.VALUE_STRING) { + return context.reportInputMismatch( + UnionWithUnknownString.class, "Union discriminator 'type' must be a string"); + } + String type = parser.getText(); + parser.nextToken(); + return deserializeSelected(parser, context, type); + } + return deserializeBuffered(parser, context); + } + + private UnionWithUnknownString deserializeBuffered(JsonParser parser, DeserializationContext context) + throws IOException { + try (TokenBuffer buffer = context.bufferForInputBuffering(parser)) { + buffer.writeStartObject(); + JsonToken token = parser.currentToken(); + while (token == JsonToken.FIELD_NAME) { + String fieldName = parser.currentName(); + JsonToken valueToken = parser.nextToken(); + if (isTypeField(fieldName, context)) { + if (valueToken != JsonToken.VALUE_STRING) { + return context.reportInputMismatch( + UnionWithUnknownString.class, "Union discriminator 'type' must be a string"); + } + String type = parser.getText(); + parser.nextToken(); + try (JsonParser bufferedParser = buffer.asParser(parser)) { + JsonParser combinedParser = + JsonParserSequence.createFlattened(true, bufferedParser, parser); + combinedParser.nextToken(); + return deserializeSelected(combinedParser, context, type); + } + } + buffer.writeFieldName(fieldName); + buffer.copyCurrentStructure(parser); + token = parser.nextToken(); + } + if (token != JsonToken.END_OBJECT) { + return context.reportInputMismatch( + UnionWithUnknownString.class, + "Expected the end of a JSON object while deserializing a union"); + } + } + return context.reportInputMismatch(UnionWithUnknownString.class, "Union discriminator 'type' is required"); + } + + private static boolean isTypeField(String fieldName, DeserializationContext context) { + return "type".equals(fieldName) + || (context.isEnabled(MapperFeature.ACCEPT_CASE_INSENSITIVE_PROPERTIES) + && "type".equalsIgnoreCase(fieldName)); + } + + private UnionWithUnknownString deserializeSelected( + JsonParser parser, DeserializationContext context, String type) throws IOException { + int variantIndex = + switch (type) { + case "unknown" -> 0; + default -> -1; + }; + if (variantIndex < 0) { + return deserializeUnknown(parser, context, type); + } + JsonDeserializer deserializer = deserializers[variantIndex]; + if (deserializer == null) { + deserializer = resolveDeserializer(context, variantIndex); + } + return new UnionWithUnknownString((Base) deserializer.deserialize(parser, context)); + } + + private synchronized JsonDeserializer resolveDeserializer(DeserializationContext context, int variantIndex) + throws JsonMappingException { + JsonDeserializer deserializer = deserializers[variantIndex]; + if (deserializer == null) { + deserializer = context.findRootValueDeserializer(context.constructType(VARIANT_TYPES[variantIndex])); + JsonDeserializer[] updated = deserializers.clone(); + updated[variantIndex] = deserializer; + deserializers = updated; + } + return deserializer; + } + + private static UnionWithUnknownString deserializeUnknown( + JsonParser parser, DeserializationContext context, String type) throws IOException { + Map values = new HashMap<>(); + if (parser.currentToken() == JsonToken.START_OBJECT) { + parser.nextToken(); + } + while (parser.currentToken() == JsonToken.FIELD_NAME) { + String fieldName = parser.currentName(); + parser.nextToken(); + values.put(fieldName, context.readValue(parser, Object.class)); + parser.nextToken(); + } + if (parser.currentToken() != JsonToken.END_OBJECT) { + return context.reportInputMismatch( + UnionWithUnknownString.class, "Expected the end of a JSON object while deserializing a union"); + } + return new UnionWithUnknownString(new UnknownWrapper(type, values)); + } + } } diff --git a/conjure-java-core/src/integrationInput/java/undertow/com/palantir/product/SimpleUnion.java b/conjure-java-core/src/integrationInput/java/undertow/com/palantir/product/SimpleUnion.java index 83e645751..89fdd76b5 100644 --- a/conjure-java-core/src/integrationInput/java/undertow/com/palantir/product/SimpleUnion.java +++ b/conjure-java-core/src/integrationInput/java/undertow/com/palantir/product/SimpleUnion.java @@ -6,15 +6,24 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.annotation.JsonSetter; -import com.fasterxml.jackson.annotation.JsonSubTypes; -import com.fasterxml.jackson.annotation.JsonTypeInfo; import com.fasterxml.jackson.annotation.JsonTypeName; import com.fasterxml.jackson.annotation.JsonValue; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.JsonToken; +import com.fasterxml.jackson.core.util.JsonParserSequence; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonDeserializer; +import com.fasterxml.jackson.databind.JsonMappingException; +import com.fasterxml.jackson.databind.MapperFeature; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.deser.ResolvableDeserializer; +import com.fasterxml.jackson.databind.util.TokenBuffer; import com.palantir.conjure.java.lib.SafeLong; import com.palantir.logsafe.Preconditions; import com.palantir.logsafe.Safe; import com.palantir.logsafe.SafeArg; import com.palantir.logsafe.exceptions.SafeIllegalArgumentException; +import java.io.IOException; import java.util.Collections; import java.util.HashMap; import java.util.Map; @@ -25,6 +34,7 @@ import javax.annotation.processing.Generated; @Generated("com.palantir.conjure.java.types.UnionGenerator") +@JsonDeserialize(using = SimpleUnion.Deserializer.class) public final class SimpleUnion { private final Base value; @@ -206,23 +216,12 @@ public interface Completed_StageVisitorBuilder { Visitor build(); } - @JsonTypeInfo( - use = JsonTypeInfo.Id.NAME, - include = JsonTypeInfo.As.EXISTING_PROPERTY, - property = "type", - visible = true, - defaultImpl = UnknownWrapper.class) - @JsonSubTypes({ - @JsonSubTypes.Type(FooWrapper.class), - @JsonSubTypes.Type(BarWrapper.class), - @JsonSubTypes.Type(BazWrapper.class) - }) - @JsonIgnoreProperties(ignoreUnknown = true) private interface Base { T accept(Visitor visitor); } @JsonTypeName("foo") + @JsonIgnoreProperties(ignoreUnknown = true) private static final class FooWrapper implements Base { private final String value; @@ -268,6 +267,7 @@ public String toString() { } @JsonTypeName("bar") + @JsonIgnoreProperties(ignoreUnknown = true) private static final class BarWrapper implements Base { private final int value; @@ -313,6 +313,7 @@ public String toString() { } @JsonTypeName("baz") + @JsonIgnoreProperties(ignoreUnknown = true) private static final class BazWrapper implements Base { private final SafeLong value; @@ -416,4 +417,129 @@ public String toString() { return "UnknownWrapper{type: " + type + ", value: " + value + '}'; } } + + static final class Deserializer extends JsonDeserializer implements ResolvableDeserializer { + private static final Class[] VARIANT_TYPES = + new Class[] {FooWrapper.class, BarWrapper.class, BazWrapper.class}; + + private volatile JsonDeserializer[] deserializers; + + @Override + public void resolve(DeserializationContext context) throws JsonMappingException { + deserializers = new JsonDeserializer[VARIANT_TYPES.length]; + } + + @Override + public boolean isCachable() { + return true; + } + + @Override + public SimpleUnion deserialize(JsonParser parser, DeserializationContext context) throws IOException { + if (!parser.isExpectedStartObjectToken()) { + return context.reportInputMismatch( + SimpleUnion.class, "Expected a JSON object for union deserialization"); + } + JsonToken firstToken = parser.nextToken(); + if (firstToken == JsonToken.FIELD_NAME && isTypeField(parser.currentName(), context)) { + if (parser.nextToken() != JsonToken.VALUE_STRING) { + return context.reportInputMismatch( + SimpleUnion.class, "Union discriminator 'type' must be a string"); + } + String type = parser.getText(); + parser.nextToken(); + return deserializeSelected(parser, context, type); + } + return deserializeBuffered(parser, context); + } + + private SimpleUnion deserializeBuffered(JsonParser parser, DeserializationContext context) throws IOException { + try (TokenBuffer buffer = context.bufferForInputBuffering(parser)) { + buffer.writeStartObject(); + JsonToken token = parser.currentToken(); + while (token == JsonToken.FIELD_NAME) { + String fieldName = parser.currentName(); + JsonToken valueToken = parser.nextToken(); + if (isTypeField(fieldName, context)) { + if (valueToken != JsonToken.VALUE_STRING) { + return context.reportInputMismatch( + SimpleUnion.class, "Union discriminator 'type' must be a string"); + } + String type = parser.getText(); + parser.nextToken(); + try (JsonParser bufferedParser = buffer.asParser(parser)) { + JsonParser combinedParser = + JsonParserSequence.createFlattened(true, bufferedParser, parser); + combinedParser.nextToken(); + return deserializeSelected(combinedParser, context, type); + } + } + buffer.writeFieldName(fieldName); + buffer.copyCurrentStructure(parser); + token = parser.nextToken(); + } + if (token != JsonToken.END_OBJECT) { + return context.reportInputMismatch( + SimpleUnion.class, "Expected the end of a JSON object while deserializing a union"); + } + } + return context.reportInputMismatch(SimpleUnion.class, "Union discriminator 'type' is required"); + } + + private static boolean isTypeField(String fieldName, DeserializationContext context) { + return "type".equals(fieldName) + || (context.isEnabled(MapperFeature.ACCEPT_CASE_INSENSITIVE_PROPERTIES) + && "type".equalsIgnoreCase(fieldName)); + } + + private SimpleUnion deserializeSelected(JsonParser parser, DeserializationContext context, String type) + throws IOException { + int variantIndex = + switch (type) { + case "foo" -> 0; + case "bar" -> 1; + case "baz" -> 2; + default -> -1; + }; + if (variantIndex < 0) { + return deserializeUnknown(parser, context, type); + } + JsonDeserializer deserializer = deserializers[variantIndex]; + if (deserializer == null) { + deserializer = resolveDeserializer(context, variantIndex); + } + return new SimpleUnion((Base) deserializer.deserialize(parser, context)); + } + + private synchronized JsonDeserializer resolveDeserializer(DeserializationContext context, int variantIndex) + throws JsonMappingException { + JsonDeserializer deserializer = deserializers[variantIndex]; + if (deserializer == null) { + deserializer = context.findRootValueDeserializer(context.constructType(VARIANT_TYPES[variantIndex])); + JsonDeserializer[] updated = deserializers.clone(); + updated[variantIndex] = deserializer; + deserializers = updated; + } + return deserializer; + } + + private static SimpleUnion deserializeUnknown(JsonParser parser, DeserializationContext context, String type) + throws IOException { + Map values = new HashMap<>(); + if (parser.currentToken() == JsonToken.START_OBJECT) { + parser.nextToken(); + } + while (parser.currentToken() == JsonToken.FIELD_NAME) { + String fieldName = parser.currentName(); + parser.nextToken(); + values.put(fieldName, context.readValue(parser, Object.class)); + parser.nextToken(); + } + if (parser.currentToken() != JsonToken.END_OBJECT) { + return context.reportInputMismatch( + SimpleUnion.class, "Expected the end of a JSON object while deserializing a union"); + } + return new SimpleUnion(new UnknownWrapper(type, values)); + } + } } diff --git a/conjure-java-core/src/main/java/com/palantir/conjure/java/types/BeanBuilderGenerator.java b/conjure-java-core/src/main/java/com/palantir/conjure/java/types/BeanBuilderGenerator.java index 5854ad671..8f697fbe4 100644 --- a/conjure-java-core/src/main/java/com/palantir/conjure/java/types/BeanBuilderGenerator.java +++ b/conjure-java-core/src/main/java/com/palantir/conjure/java/types/BeanBuilderGenerator.java @@ -193,7 +193,7 @@ private void addStagedBuilderImpl( List allFields = enrichFields(typeDef.getFields()); List fieldsNeedingBuilderStage = allFields.stream() .filter(field -> !BeanBuilderGenerator.stagedBuilderFieldShouldBeInFinalStage(field)) - .collect(Collectors.toList()); + .toList(); if (fieldsNeedingBuilderStage.isEmpty()) { addBuilderImpl(specBuilder, typeDef, typesMap); @@ -206,7 +206,7 @@ private void addStagedBuilderImpl( interfaces.add(generateBuilderFinalStageInterface( allFields.stream() .filter(field -> !fieldsNeedingBuilderStage.contains(field)) - .collect(Collectors.toList()), + .toList(), false)); specBuilder.addTypes(interfaces); addBuilderInterfaceAndMethod(specBuilder, interfaces); @@ -234,7 +234,7 @@ private void addBuilderInterfaceAndMethod(TypeSpec.Builder specBuilder, List ClassName.get(objectClass.packageName(), objectClass.simpleName(), stageInterface.name())) .map(Primitives::box) - .collect(Collectors.toList()); + .toList(); TypeSpec builderInterface = TypeSpec.interfaceBuilder(STAGED_BUILDER_INTERFACE_NAME) .addModifiers(Modifier.PUBLIC) @@ -251,7 +251,7 @@ private void addBuilderInterfaceAndMethod(TypeSpec.Builder specBuilder, List fields, .addMethods(fields.stream() .map(field -> generateMethodsForFinalStageInterfaceField(field, completedStageClass, strict)) .flatMap(List::stream) - .collect(Collectors.toList())) + .toList()) .build(); } @@ -423,6 +423,13 @@ private TypeSpec generateBuilderImplementation( boolean strict) { Collection enrichedFields = enrichFields(typeDef.getFields()); Collection poetFields = EnrichedField.toPoetSpecs(enrichedFields); + Collection builderFields = enrichedFields.stream() + .map(field -> field.conjureDef().getType().accept(TypeVisitor.IS_MAP) + ? field.poetSpec().toBuilder() + .addAnnotation(createJacksonSetterAnnotation(field, typesMap)) + .build() + : field.poetSpec()) + .toList(); TypeSpec.Builder specBuilder = TypeSpec.classBuilder(className); boolean implementsInterface = builderInterfaceClass.isPresent(); @@ -438,7 +445,7 @@ private TypeSpec generateBuilderImplementation( .addAnnotation(ConjureAnnotations.getConjureGeneratedAnnotation(BeanBuilderGenerator.class)) .addModifiers(Modifier.STATIC, Modifier.FINAL) .addField(FieldSpec.builder(boolean.class, BUILT_FIELD).build()) - .addFields(poetFields) + .addFields(builderFields) .addFields(primitivesInitializedFields(enrichedFields)) .addMethod(createConstructor()) .addMethod(createFromObject(enrichedFields, implementsInterface)) @@ -457,7 +464,7 @@ private TypeSpec generateBuilderImplementation( private Collection maybeCreateValidateFieldsMethods(Collection enrichedFields) { List primitives = - enrichedFields.stream().filter(EnrichedField::isPrimitive).collect(Collectors.toList()); + enrichedFields.stream().filter(EnrichedField::isPrimitive).toList(); if (primitives.isEmpty()) { return Collections.emptyList(); @@ -522,7 +529,7 @@ private Collection primitivesInitializedFields(Collection FieldSpec.builder(TypeName.BOOLEAN, deriveFieldInitializedName(field), Modifier.PRIVATE) .initializer("false") .build()) - .collect(Collectors.toList()); + .toList(); } private static String deriveFieldInitializedName(EnrichedField field) { @@ -655,7 +662,7 @@ private MethodSpec createSetter( // Certain type combinations and feature flags may produce highly optimized code paths. // These optimized paths include deserialization, so if it isn't enabled we should add the // simple deserializer. - if (!isPrimitiveOptimized(type)) { + if (!isPrimitiveOptimized(type) && !type.accept(TypeVisitor.IS_MAP)) { setterBuilder.addAnnotation(createJacksonSetterAnnotation(enriched, typesMap)); } diff --git a/conjure-java-core/src/main/java/com/palantir/conjure/java/types/UnionGenerator.java b/conjure-java-core/src/main/java/com/palantir/conjure/java/types/UnionGenerator.java index f7c273462..a6d0a37e5 100644 --- a/conjure-java-core/src/main/java/com/palantir/conjure/java/types/UnionGenerator.java +++ b/conjure-java-core/src/main/java/com/palantir/conjure/java/types/UnionGenerator.java @@ -21,12 +21,23 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.annotation.JsonSetter; -import com.fasterxml.jackson.annotation.JsonSubTypes; -import com.fasterxml.jackson.annotation.JsonTypeInfo; import com.fasterxml.jackson.annotation.JsonTypeName; import com.fasterxml.jackson.annotation.JsonValue; import com.fasterxml.jackson.annotation.Nulls; +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.JsonToken; +import com.fasterxml.jackson.core.util.JsonParserSequence; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonDeserializer; +import com.fasterxml.jackson.databind.JsonMappingException; +import com.fasterxml.jackson.databind.JsonSerializer; +import com.fasterxml.jackson.databind.MapperFeature; +import com.fasterxml.jackson.databind.SerializerProvider; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import com.fasterxml.jackson.databind.deser.ResolvableDeserializer; +import com.fasterxml.jackson.databind.util.TokenBuffer; import com.google.common.collect.ImmutableList; import com.google.common.collect.Iterators; import com.google.common.collect.PeekingIterator; @@ -47,6 +58,7 @@ import com.palantir.conjure.spec.UnionDefinition; import com.palantir.conjure.visitor.TypeVisitor; import com.palantir.javapoet.AnnotationSpec; +import com.palantir.javapoet.ArrayTypeName; import com.palantir.javapoet.ClassName; import com.palantir.javapoet.CodeBlock; import com.palantir.javapoet.FieldSpec; @@ -57,9 +69,11 @@ import com.palantir.javapoet.TypeName; import com.palantir.javapoet.TypeSpec; import com.palantir.javapoet.TypeVariableName; +import com.palantir.javapoet.WildcardTypeName; import com.palantir.logsafe.Safe; import com.palantir.logsafe.SafeArg; import com.palantir.logsafe.exceptions.SafeIllegalArgumentException; +import java.io.IOException; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; @@ -96,6 +110,8 @@ public final class UnionGenerator { private static final String SEALED_KNOWN_INTERFACE = "Known"; private static final String SEALED_UNKNOWN_VARIANT_NAME = "Unknown"; + private static final String DESERIALIZER_CLASS_NAME = "Deserializer"; + private static final String SERIALIZER_CLASS_NAME = "Serializer"; public static JavaFile generateUnionType( TypeMapper typeMapper, @@ -126,8 +142,8 @@ public static JavaFile generateUnionType( typeDef.getTypeName().getName()) .addAnnotations(safety) .addAnnotation(ConjureAnnotations.getConjureGeneratedAnnotation(UnionGenerator.class)) - .addAnnotation(generateJsonTypeInfo(unknownVariant)) - .addAnnotation(generateJsonSubTypes(unionClass, typeDef.getUnion())) + .addAnnotation(generateJsonDeserialize(unionClass)) + .addAnnotation(generateJsonSerialize(unionClass)) .addAnnotation(ignoreUnknownAnnotation()) .addModifiers(Modifier.PUBLIC, Modifier.ABSTRACT, Modifier.SEALED) .addPermittedSubclasses(typeDef.getUnion().stream() @@ -140,7 +156,9 @@ public static JavaFile generateUnionType( .addMethods(generateSealedThrowOnUnknown(unionClass, unknownVariant, typeDef.getUnion())) .addTypes(generateWrapperClasses( typeMapper, typesMap, unionClass, visitorClass, typeDef.getUnion(), options)) - .addType(generateUnknownWrapper(unionClass, visitorClass, options)); + .addType(generateUnknownWrapper(unionClass, visitorClass, options)) + .addType(generateSerializer(unionClass)) + .addType(generateDeserializer(unionClass, typeDef.getUnion(), options)); typeDef.getDocs().ifPresent(docs -> typeBuilder.addJavadoc("$L", Javadoc.render(docs))); @@ -175,6 +193,7 @@ public static JavaFile generateUnionType( typeDef.getTypeName().getName()) .addAnnotations(safety) .addAnnotation(ConjureAnnotations.getConjureGeneratedAnnotation(UnionGenerator.class)) + .addAnnotation(generateJsonDeserialize(unionClass)) .addModifiers(Modifier.PUBLIC, Modifier.FINAL) .addFields(fields) .addMethod(generateConstructor(baseClass)) @@ -193,6 +212,7 @@ public static JavaFile generateUnionType( .addTypes(generateWrapperClasses( typeMapper, typesMap, baseClass, visitorClass, typeDef.getUnion(), options)) .addType(generateUnknownWrapper(baseClass, visitorClass, options)) + .addType(generateDeserializer(unionClass, typeDef.getUnion(), options)) .addMethod(MethodSpecs.createEquals(unionClass)) .addMethod(MethodSpecs.createEqualTo(unionClass, fields)) .addMethod(MethodSpecs.createHashCode(fields)) @@ -236,33 +256,16 @@ private static MethodSpec createLegacyToStringForSealedUnions(ClassName baseClas .build(); } - private static AnnotationSpec generateJsonTypeInfo(ClassName unknownVariant) { - return AnnotationSpec.builder(JsonTypeInfo.class) - .addMember("use", "JsonTypeInfo.Id.NAME") - .addMember("include", "JsonTypeInfo.As.EXISTING_PROPERTY") - .addMember("property", "\"type\"") - .addMember("visible", "$L", true) - .addMember("defaultImpl", "$T.class", unknownVariant) + private static AnnotationSpec generateJsonDeserialize(ClassName unionClass) { + return AnnotationSpec.builder(JsonDeserialize.class) + .addMember("using", "$T.class", unionClass.nestedClass(DESERIALIZER_CLASS_NAME)) .build(); } - private static AnnotationSpec generateJsonSubTypes(ClassName unionClass, List memberTypeDefs) { - List subAnnotations = memberTypeDefs.stream() - .map(memberTypeDef -> AnnotationSpec.builder(JsonSubTypes.Type.class) - .addMember("value", "$T.class", sealedVariantClass(unionClass, memberTypeDef.getFieldName())) - .addMember( - "name", - "$S", - // "unknown" is valid here since UnknownVariant is only used as a default - memberTypeDef.getFieldName().get()) - .build()) - .toList(); - AnnotationSpec.Builder annotationBuilder = AnnotationSpec.builder(JsonSubTypes.class); - subAnnotations.forEach(subAnnotation -> annotationBuilder.addMember("value", "$L", subAnnotation)); - if (subAnnotations.isEmpty()) { - annotationBuilder.addMember("value", "{}"); - } - return annotationBuilder.build(); + private static AnnotationSpec generateJsonSerialize(ClassName unionClass) { + return AnnotationSpec.builder(JsonSerialize.class) + .addMember("using", "$T.class", unionClass.nestedClass(SERIALIZER_CLASS_NAME)) + .build(); } private static AnnotationSpec ignoreUnknownAnnotation() { @@ -829,6 +832,277 @@ private static Stream sortedStageNameTypePairs(Map memberTypeDefs, Options options) { + ClassName deserializerClass = unionClass.nestedClass(DESERIALIZER_CLASS_NAME); + TypeSpec.Builder builder = TypeSpec.classBuilder(deserializerClass) + .addModifiers(Modifier.STATIC, Modifier.FINAL) + .superclass(ParameterizedTypeName.get(ClassName.get(JsonDeserializer.class), unionClass)) + .addSuperinterface(ResolvableDeserializer.class) + .addField(generateVariantTypesField(unionClass, memberTypeDefs, options)) + .addField(generateDeserializersField()) + .addMethod(generateResolveMethod()) + .addMethod(generateIsCachableMethod()) + .addMethod(generateDeserializeMethod(unionClass)) + .addMethod(generateDeserializeBufferedMethod(unionClass)) + .addMethod(generateIsTypeFieldMethod()) + .addMethod(generateDeserializeSelectedMethod(unionClass, memberTypeDefs, options)) + .addMethod(generateResolveDeserializerMethod()) + .addMethod(generateDeserializeUnknownMethod(unionClass, options)); + return builder.build(); + } + + private static FieldSpec generateVariantTypesField( + ClassName unionClass, List memberTypeDefs, Options options) { + TypeName classType = + ParameterizedTypeName.get(ClassName.get(Class.class), WildcardTypeName.subtypeOf(Object.class)); + CodeBlock.Builder initializer = CodeBlock.builder().add("new $T[] {$>", Class.class); + for (int index = 0; index < memberTypeDefs.size(); index++) { + FieldDefinition memberTypeDef = memberTypeDefs.get(index); + ClassName wrapperClass = options.sealedUnions() + ? sealedVariantClass(unionClass, memberTypeDef.getFieldName()) + : wrapperClass(unionClass, sanitizeUnknown(memberTypeDef.getFieldName())); + if (index > 0) { + initializer.add(","); + } + initializer.add("\n$T.class", wrapperClass); + } + initializer.add("$<\n}"); + return FieldSpec.builder( + ArrayTypeName.of(classType), "VARIANT_TYPES", Modifier.PRIVATE, Modifier.STATIC, Modifier.FINAL) + .initializer("$L", initializer.build()) + .build(); + } + + private static FieldSpec generateDeserializersField() { + TypeName deserializerType = ParameterizedTypeName.get( + ClassName.get(JsonDeserializer.class), WildcardTypeName.subtypeOf(Object.class)); + return FieldSpec.builder( + ArrayTypeName.of(deserializerType), "deserializers", Modifier.PRIVATE, Modifier.VOLATILE) + .build(); + } + + private static MethodSpec generateResolveMethod() { + return MethodSpec.methodBuilder("resolve") + .addAnnotation(Override.class) + .addModifiers(Modifier.PUBLIC) + .addParameter(DeserializationContext.class, "context") + .addException(JsonMappingException.class) + .addStatement("deserializers = new $T[VARIANT_TYPES.length]", JsonDeserializer.class) + .build(); + } + + private static MethodSpec generateIsCachableMethod() { + return MethodSpec.methodBuilder("isCachable") + .addAnnotation(Override.class) + .addModifiers(Modifier.PUBLIC) + .returns(TypeName.BOOLEAN) + .addStatement("return true") + .build(); + } + + private static TypeSpec generateSerializer(ClassName unionClass) { + return TypeSpec.classBuilder(unionClass.nestedClass(SERIALIZER_CLASS_NAME)) + .addModifiers(Modifier.STATIC, Modifier.FINAL) + .superclass(ParameterizedTypeName.get(ClassName.get(JsonSerializer.class), unionClass)) + .addMethod(MethodSpec.methodBuilder("serialize") + .addAnnotation(Override.class) + .addModifiers(Modifier.PUBLIC) + .addParameter(unionClass, "value") + .addParameter(JsonGenerator.class, "generator") + .addParameter(SerializerProvider.class, "serializers") + .addException(IOException.class) + .addStatement("serializers.findValueSerializer(value.getClass()).serialize(value, generator," + + " serializers)") + .build()) + .build(); + } + + private static MethodSpec generateDeserializeMethod(ClassName unionClass) { + return MethodSpec.methodBuilder("deserialize") + .addAnnotation(Override.class) + .addModifiers(Modifier.PUBLIC) + .returns(unionClass) + .addParameter(JsonParser.class, "parser") + .addParameter(DeserializationContext.class, "context") + .addException(IOException.class) + .beginControlFlow("if (!parser.isExpectedStartObjectToken())") + .addStatement( + "return context.reportInputMismatch($T.class, $S)", + unionClass, + "Expected a JSON object for union deserialization") + .endControlFlow() + .addStatement("$T firstToken = parser.nextToken()", JsonToken.class) + .beginControlFlow( + "if (firstToken == $T.FIELD_NAME && isTypeField(parser.currentName(), context))", + JsonToken.class) + .beginControlFlow("if (parser.nextToken() != $T.VALUE_STRING)", JsonToken.class) + .addStatement( + "return context.reportInputMismatch($T.class, $S)", + unionClass, + "Union discriminator 'type' must be a string") + .endControlFlow() + .addStatement("$T type = parser.getText()", String.class) + .addStatement("parser.nextToken()") + .addStatement("return deserializeSelected(parser, context, type)") + .endControlFlow() + .addStatement("return deserializeBuffered(parser, context)") + .build(); + } + + private static MethodSpec generateDeserializeBufferedMethod(ClassName unionClass) { + return MethodSpec.methodBuilder("deserializeBuffered") + .addModifiers(Modifier.PRIVATE) + .returns(unionClass) + .addParameter(JsonParser.class, "parser") + .addParameter(DeserializationContext.class, "context") + .addException(IOException.class) + .beginControlFlow("try ($T buffer = context.bufferForInputBuffering(parser))", TokenBuffer.class) + .addStatement("buffer.writeStartObject()") + .addStatement("$T token = parser.currentToken()", JsonToken.class) + .beginControlFlow("while (token == $T.FIELD_NAME)", JsonToken.class) + .addStatement("$T fieldName = parser.currentName()", String.class) + .addStatement("$T valueToken = parser.nextToken()", JsonToken.class) + .beginControlFlow("if (isTypeField(fieldName, context))") + .beginControlFlow("if (valueToken != $T.VALUE_STRING)", JsonToken.class) + .addStatement( + "return context.reportInputMismatch($T.class, $S)", + unionClass, + "Union discriminator 'type' must be a string") + .endControlFlow() + .addStatement("$T type = parser.getText()", String.class) + .addStatement("parser.nextToken()") + .beginControlFlow("try ($T bufferedParser = buffer.asParser(parser))", JsonParser.class) + .addStatement( + "$T combinedParser = $T.createFlattened(true, bufferedParser, parser)", + JsonParser.class, + JsonParserSequence.class) + .addStatement("combinedParser.nextToken()") + .addStatement("return deserializeSelected(combinedParser, context, type)") + .endControlFlow() + .endControlFlow() + .addStatement("buffer.writeFieldName(fieldName)") + .addStatement("buffer.copyCurrentStructure(parser)") + .addStatement("token = parser.nextToken()") + .endControlFlow() + .beginControlFlow("if (token != $T.END_OBJECT)", JsonToken.class) + .addStatement( + "return context.reportInputMismatch($T.class, $S)", + unionClass, + "Expected the end of a JSON object while deserializing a union") + .endControlFlow() + .endControlFlow() + .addStatement( + "return context.reportInputMismatch($T.class, $S)", + unionClass, + "Union discriminator 'type' is required") + .build(); + } + + private static MethodSpec generateIsTypeFieldMethod() { + return MethodSpec.methodBuilder("isTypeField") + .addModifiers(Modifier.PRIVATE, Modifier.STATIC) + .returns(TypeName.BOOLEAN) + .addParameter(String.class, "fieldName") + .addParameter(DeserializationContext.class, "context") + .addStatement( + "return $S.equals(fieldName) || (context.isEnabled($T.ACCEPT_CASE_INSENSITIVE_PROPERTIES)" + + " && $S.equalsIgnoreCase(fieldName))", + "type", + MapperFeature.class, + "type") + .build(); + } + + private static MethodSpec generateDeserializeSelectedMethod( + ClassName unionClass, List memberTypeDefs, Options options) { + MethodSpec.Builder builder = MethodSpec.methodBuilder("deserializeSelected") + .addModifiers(Modifier.PRIVATE) + .returns(unionClass) + .addParameter(JsonParser.class, "parser") + .addParameter(DeserializationContext.class, "context") + .addParameter(String.class, "type") + .addException(IOException.class) + .addCode("int variantIndex = switch (type) {\n"); + for (int index = 0; index < memberTypeDefs.size(); index++) { + builder.addStatement("case $S -> $L", memberTypeDefs.get(index).getFieldName(), index); + } + builder.addStatement("default -> -1").addCode("};\n"); + builder.beginControlFlow("if (variantIndex < 0)"); + builder.addStatement("return deserializeUnknown(parser, context, type)"); + builder.endControlFlow(); + builder.addStatement("$T deserializer = deserializers[variantIndex]", JsonDeserializer.class) + .beginControlFlow("if (deserializer == null)") + .addStatement("deserializer = resolveDeserializer(context, variantIndex)") + .endControlFlow(); + if (options.sealedUnions()) { + builder.addStatement("return ($T) deserializer.deserialize(parser, context)", unionClass); + } else { + builder.addStatement( + "return new $T(($T) deserializer.deserialize(parser, context))", + unionClass, + unionClass.nestedClass("Base")); + } + return builder.build(); + } + + private static MethodSpec generateResolveDeserializerMethod() { + ParameterizedTypeName deserializerType = ParameterizedTypeName.get( + ClassName.get(JsonDeserializer.class), WildcardTypeName.subtypeOf(Object.class)); + return MethodSpec.methodBuilder("resolveDeserializer") + .addModifiers(Modifier.PRIVATE, Modifier.SYNCHRONIZED) + .returns(deserializerType) + .addParameter(DeserializationContext.class, "context") + .addParameter(TypeName.INT, "variantIndex") + .addException(JsonMappingException.class) + .addStatement("$T deserializer = deserializers[variantIndex]", deserializerType) + .beginControlFlow("if (deserializer == null)") + .addStatement("deserializer = context.findRootValueDeserializer(" + + "context.constructType(VARIANT_TYPES[variantIndex]))") + .addStatement("$T[] updated = deserializers.clone()", deserializerType) + .addStatement("updated[variantIndex] = deserializer") + .addStatement("deserializers = updated") + .endControlFlow() + .addStatement("return deserializer") + .build(); + } + + private static MethodSpec generateDeserializeUnknownMethod(ClassName unionClass, Options options) { + ParameterizedTypeName valueMapType = ParameterizedTypeName.get(Map.class, String.class, Object.class); + MethodSpec.Builder builder = MethodSpec.methodBuilder("deserializeUnknown") + .addModifiers(Modifier.PRIVATE, Modifier.STATIC) + .returns(unionClass) + .addParameter(JsonParser.class, "parser") + .addParameter(DeserializationContext.class, "context") + .addParameter(String.class, "type") + .addException(IOException.class) + .addStatement("$T values = new $T<>()", valueMapType, HashMap.class) + .beginControlFlow("if (parser.currentToken() == $T.START_OBJECT)", JsonToken.class) + .addStatement("parser.nextToken()") + .endControlFlow() + .beginControlFlow("while (parser.currentToken() == $T.FIELD_NAME)", JsonToken.class) + .addStatement("$T fieldName = parser.currentName()", String.class) + .addStatement("parser.nextToken()") + .addStatement("values.put(fieldName, context.readValue(parser, $T.class))", Object.class) + .addStatement("parser.nextToken()") + .endControlFlow() + .beginControlFlow("if (parser.currentToken() != $T.END_OBJECT)", JsonToken.class) + .addStatement( + "return context.reportInputMismatch($T.class, $S)", + unionClass, + "Expected the end of a JSON object while deserializing a union") + .endControlFlow(); + if (options.sealedUnions()) { + builder.addStatement("return new $T(type, values)", unionClass.nestedClass(SEALED_UNKNOWN_VARIANT_NAME)); + } else { + builder.addStatement( + "return new $T(new $T(type, values))", + unionClass, + unionClass.nestedClass(UNKNOWN_WRAPPER_CLASS_NAME)); + } + return builder.build(); + } + /** * Generates a prototype for a visitor builder setter that can be turned into an interface method declaration or an * implementation of such interface method. The signature of the returned builder is @@ -855,34 +1129,7 @@ private static MethodSpec.Builder visitorBuilderUnknownThrowPrototype( private static TypeSpec generateBase( ClassName baseClass, ClassName visitorClass, Map memberTypes) { - ClassName unknownWrapperClass = baseClass.peerClass(UNKNOWN_WRAPPER_CLASS_NAME); - TypeSpec.Builder baseBuilder = TypeSpec.interfaceBuilder(baseClass) - .addModifiers(Modifier.PRIVATE) - .addAnnotation(AnnotationSpec.builder(JsonTypeInfo.class) - .addMember("use", "JsonTypeInfo.Id.NAME") - .addMember("include", "JsonTypeInfo.As.EXISTING_PROPERTY") - .addMember("property", "\"type\"") - .addMember("visible", "$L", true) - .addMember("defaultImpl", "$T.class", unknownWrapperClass) - .build()); - if (!memberTypes.isEmpty()) { - List subAnnotations = memberTypes.entrySet().stream() - .map(entry -> AnnotationSpec.builder(JsonSubTypes.Type.class) - .addMember( - "value", - "$T.class", - peerWrapperClass( - baseClass, - sanitizeUnknown(entry.getKey().getFieldName()))) - .build()) - .collect(Collectors.toList()); - AnnotationSpec.Builder annotationBuilder = AnnotationSpec.builder(JsonSubTypes.class); - subAnnotations.forEach(subAnnotation -> annotationBuilder.addMember("value", "$L", subAnnotation)); - baseBuilder.addAnnotation(annotationBuilder.build()); - } - baseBuilder.addAnnotation(AnnotationSpec.builder(JsonIgnoreProperties.class) - .addMember("ignoreUnknown", "$L", true) - .build()); + TypeSpec.Builder baseBuilder = TypeSpec.interfaceBuilder(baseClass).addModifiers(Modifier.PRIVATE); ParameterizedTypeName parameterizedVisitorClass = ParameterizedTypeName.get(visitorClass, TYPE_VARIABLE); ParameterSpec visitor = ParameterSpec.builder(parameterizedVisitorClass, "visitor").build(); @@ -924,6 +1171,7 @@ private static List generateWrapperClasses( .addAnnotation(AnnotationSpec.builder(JsonTypeName.class) .addMember("value", "$S", memberTypeDef.getFieldName()) .build()) + .addAnnotation(ignoreUnknownAnnotation()) .addFields(fields) .addMethod(MethodSpec.constructorBuilder() .addModifiers(Modifier.PRIVATE) @@ -959,6 +1207,13 @@ private static List generateWrapperClasses( .returns(memberType) .build()); + if (options.sealedUnions()) { + // Prevent the custom union serializer and deserializer on the sealed base class from being + // inherited by concrete variants when delegating directly to a known wrapper. + typeBuilder.addAnnotation(JsonDeserialize.class); + typeBuilder.addAnnotation(JsonSerialize.class); + } + if (!options.sealedUnions() || (options.sealedUnions() && options.sealedUnionVisitors())) { typeBuilder.addMethod(createWrapperAcceptMethod( visitorClass, @@ -1103,6 +1358,11 @@ private static TypeSpec generateUnknownWrapper(ClassName baseClass, ClassName vi .addStatement("$L.put(key, val)", VALUE_FIELD_NAME) .build()); + if (options.sealedUnions()) { + typeBuilder.addAnnotation(JsonDeserialize.class); + typeBuilder.addAnnotation(JsonSerialize.class); + } + if (!options.sealedUnions() || (options.sealedUnions() && options.sealedUnionVisitors())) { typeBuilder.addMethod(createWrapperAcceptMethod( visitorClass, VISIT_UNKNOWN_METHOD_NAME, typeParameter.name(), false, options)); diff --git a/conjure-java-server-verifier/build.gradle b/conjure-java-server-verifier/build.gradle index 0d09b6702..9d24ac07a 100644 --- a/conjure-java-server-verifier/build.gradle +++ b/conjure-java-server-verifier/build.gradle @@ -72,6 +72,7 @@ project('verification-client-api') { implementation project(':conjure-java-client-verifier:verification-server-api') implementation 'com.fasterxml.jackson.core:jackson-annotations' + implementation 'com.fasterxml.jackson.core:jackson-core' implementation 'com.fasterxml.jackson.core:jackson-databind' implementation 'com.google.code.findbugs:jsr305' implementation 'com.google.errorprone:error_prone_annotations' @@ -90,4 +91,3 @@ tasks.withType(JavaCompile).matching { it.name == "compileTestJava" }.configureE } } -