Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions .fern/metadata.json
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
{
"cliVersion": "4.90.0",
"cliVersion": "5.1.0",
"generatorName": "fernapi/fern-java-sdk",
"generatorVersion": "4.4.1",
"generatorVersion": "4.6.4",
"generatorConfig": {
"publish-to": "central"
},
"originGitCommit": "3707beee5d0e236f2469e89f7b19c3a08efb6f95",
"originGitCommit": "57be6b5662276253461696b49ad9ff6448cc29f3",
"originGitCommitIsDirty": true,
"invokedBy": "ci",
"requestedVersion": "AUTO",
"ciProvider": "unknown",
"sdkVersion": "2.1.0"
"sdkVersion": "2.1.1"
}
3 changes: 3 additions & 0 deletions .fernignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,6 @@ LICENSE

# README.md
changelog.md

# CI
.github/workflows/sdk-release.yml
123 changes: 123 additions & 0 deletions .github/workflows/sdk-release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
name: SDK Release

on:
workflow_dispatch:
pull_request:
types: [closed]
branches:
- main

jobs:
compile:
if: github.event_name == 'workflow_dispatch' || github.event.pull_request.merged == true
runs-on: ubuntu-latest
steps:
- name: Checkout repo
uses: actions/checkout@v4

- name: Set up Java
id: setup-jre
uses: actions/setup-java@v1
with:
java-version: "11"
architecture: x64

- name: Compile
run: ./gradlew compileJava

test:
needs: [compile]
if: github.event_name == 'workflow_dispatch' || github.event.pull_request.merged == true
runs-on: ubuntu-latest
steps:
- name: Checkout repo
uses: actions/checkout@v4

- name: Set up Java
id: setup-jre
uses: actions/setup-java@v1
with:
java-version: "11"
architecture: x64

- name: Test
run: ./gradlew test

release:
needs: [compile, test]
runs-on: ubuntu-latest
permissions:
contents: write

steps:
- name: Checkout main
uses: actions/checkout@v4
with:
ref: main
fetch-depth: 0

- name: Read version from build.gradle
id: version
run: |
VERSION=$(grep -m1 "^version = '" build.gradle | grep -oP "'\K[^']+")
if [ -z "$VERSION" ]; then
echo "ERROR: Could not read version from build.gradle"
exit 1
fi
echo "VERSION=$VERSION" >> $GITHUB_OUTPUT
echo "TAG=v$VERSION" >> $GITHUB_OUTPUT

- name: Get latest git tag
id: latest_tag
run: |
LATEST=$(git describe --tags --abbrev=0 2>/dev/null || echo "none")
echo "LATEST_TAG=$LATEST" >> $GITHUB_OUTPUT

- name: Check if version was bumped
id: check
run: |
TAG="${{ steps.version.outputs.TAG }}"
LATEST="${{ steps.latest_tag.outputs.LATEST_TAG }}"
if [ "$TAG" = "$LATEST" ]; then
echo "bumped=false" >> $GITHUB_OUTPUT
echo "Version $TAG already tagged — skipping release."
else
echo "bumped=true" >> $GITHUB_OUTPUT
echo "New version detected: $TAG (previous: $LATEST)"
fi

- name: Set up Java
if: steps.check.outputs.bumped == 'true'
uses: actions/setup-java@v1
with:
java-version: "11"
architecture: x64

- name: Create and push tag
if: steps.check.outputs.bumped == 'true'
run: |
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git tag "${{ steps.version.outputs.TAG }}"
git push origin "${{ steps.version.outputs.TAG }}"

- name: Publish to Maven
if: steps.check.outputs.bumped == 'true'
run: ./gradlew sonatypeCentralUpload
env:
MAVEN_USERNAME: ${{ secrets.MAVEN_USERNAME }}
MAVEN_PASSWORD: ${{ secrets.MAVEN_PASSWORD }}
MAVEN_PUBLISH_REGISTRY_URL: "https://oss.sonatype.org/service/local/staging/deploy/maven2/"
MAVEN_SIGNATURE_KID: ${{ secrets.MAVEN_SIGNATURE_KID }}
MAVEN_SIGNATURE_SECRET_KEY: ${{ secrets.MAVEN_SIGNATURE_SECRET_KEY }}
MAVEN_SIGNATURE_PASSWORD: ${{ secrets.MAVEN_SIGNATURE_PASSWORD }}

- name: Create GitHub release with generated notes
if: steps.check.outputs.bumped == 'true'
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
gh release create "${{ steps.version.outputs.TAG }}" \
--title "${{ steps.version.outputs.TAG }}" \
--generate-notes \
--latest
5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ Add the dependency in your `build.gradle` file:

```groovy
dependencies {
implementation 'com.newscatcherapi:newscatcher-sdk:2.1.0'
implementation 'com.newscatcherapi:newscatcher-sdk:2.1.1'
}
```

Expand All @@ -55,7 +55,7 @@ Add the dependency in your `pom.xml` file:
<dependency>
<groupId>com.newscatcherapi</groupId>
<artifactId>newscatcher-sdk</artifactId>
<version>2.1.0</version>
<version>2.1.1</version>
</dependency>
```

Expand Down Expand Up @@ -354,3 +354,4 @@ On the other hand, contributions to the README are always very welcome!
For release notes and changelog, visit our [GitHub releases page](https://github.com/Newscatcher/newscatcher-java/releases).



4 changes: 2 additions & 2 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ java {

group = 'com.newscatcherapi'

version = '2.1.0'
version = '2.1.1'

jar {
dependsOn(":generatePomFileForMavenPublication")
Expand Down Expand Up @@ -78,7 +78,7 @@ publishing {
maven(MavenPublication) {
groupId = 'com.newscatcherapi'
artifactId = 'newscatcher-sdk'
version = '2.1.0'
version = '2.1.1'
from components.java
pom {
name = 'newscatcher'
Expand Down
19 changes: 19 additions & 0 deletions changelog.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,22 @@
## 2.1.1 - 2026-04-30
* fix: use field-presence checks in union deserializers to prevent misclassification
* Previously, all union response deserializers attempted blind `convertValue`
* calls in sequence, catching `RuntimeException` to fall through to the next
* candidate type. This approach could silently misclassify a response payload
* as the wrong union variant when Jackson's lenient conversion succeeded on an
* incompatible type.
* The deserializers now inspect the raw `Map` keys first, requiring the
* presence of discriminating fields before attempting conversion. This makes
* variant selection deterministic and prevents incorrect deserialization of
* ambiguous payloads.
* Key changes:
* `GetAggregationCountResponse` and `PostAggregationCountResponse` deserializers now gate on `status`, `total_hits`, `page`, `total_pages`, and `page_size` keys before attempting conversion to `AggregationCountResponseDto` or `FailedAggregationCountResponseDto`
* `GetAuthorsResponse` and `PostAuthorsResponse` deserializers apply the same pagination-field guard before converting to `SearchResponseDto` or `FailedAuthorsResponseDto`
* `GetLatestHeadlinesResponse`, `PostLatestHeadlinesResponse`, `GetSearchResponse`, and `PostSearchResponse` deserializers additionally require `clusters_count` and `clusters` keys before attempting `ClusteredSearchResponseDto` conversion
* `AggregationCountResponseDtoAggregations` deserializer gates on `aggregation_count` key before attempting `AggregationItem` conversion
* `SourcesResponseDtoSourcesItem` deserializer gates on `domain_url` key before attempting `SourceInfo` conversion
* 🌿 Generated with Fern

## 2.1.0 - 2026-04-23
* ## [2.1.0] - 2025-07-10
### Added
Expand Down
4 changes: 2 additions & 2 deletions src/main/java/com/newscatcher/api/core/ClientOptions.java
Original file line number Diff line number Diff line change
Expand Up @@ -38,10 +38,10 @@ private ClientOptions(
this.headers.putAll(headers);
this.headers.putAll(new HashMap<String, String>() {
{
put("User-Agent", "com.newscatcherapi:newscatcher-sdk/2.1.0");
put("User-Agent", "com.newscatcherapi:newscatcher-sdk/2.1.1");
put("X-Fern-Language", "JAVA");
put("X-Fern-SDK-Name", "com.newscatcher.fern:api-sdk");
put("X-Fern-SDK-Version", "2.1.0");
put("X-Fern-SDK-Version", "2.1.1");
}
});
this.headerSuppliers = headerSuppliers;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
import com.newscatcher.api.types.AggregationCountResponseDto;
import com.newscatcher.api.types.FailedAggregationCountResponseDto;
import java.io.IOException;
import java.util.Map;
import java.util.Objects;

@JsonDeserialize(using = GetAggregationCountResponse.Deserializer.class)
Expand Down Expand Up @@ -84,13 +85,27 @@ static final class Deserializer extends StdDeserializer<GetAggregationCountRespo
public GetAggregationCountResponse deserialize(JsonParser p, DeserializationContext context)
throws IOException {
Object value = p.readValueAs(Object.class);
try {
return of(ObjectMappers.JSON_MAPPER.convertValue(value, AggregationCountResponseDto.class));
} catch (RuntimeException e) {
if (value instanceof Map<?, ?>
&& ((Map<?, ?>) value).containsKey("status")
&& ((Map<?, ?>) value).containsKey("total_hits")
&& ((Map<?, ?>) value).containsKey("page")
&& ((Map<?, ?>) value).containsKey("total_pages")
&& ((Map<?, ?>) value).containsKey("page_size")) {
try {
return of(ObjectMappers.JSON_MAPPER.convertValue(value, AggregationCountResponseDto.class));
} catch (RuntimeException e) {
}
}
try {
return of(ObjectMappers.JSON_MAPPER.convertValue(value, FailedAggregationCountResponseDto.class));
} catch (RuntimeException e) {
if (value instanceof Map<?, ?>
&& ((Map<?, ?>) value).containsKey("status")
&& ((Map<?, ?>) value).containsKey("total_hits")
&& ((Map<?, ?>) value).containsKey("page")
&& ((Map<?, ?>) value).containsKey("total_pages")
&& ((Map<?, ?>) value).containsKey("page_size")) {
try {
return of(ObjectMappers.JSON_MAPPER.convertValue(value, FailedAggregationCountResponseDto.class));
} catch (RuntimeException e) {
}
}
throw new JsonParseException(p, "Failed to deserialize");
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
import com.newscatcher.api.types.AggregationCountResponseDto;
import com.newscatcher.api.types.FailedAggregationCountResponseDto;
import java.io.IOException;
import java.util.Map;
import java.util.Objects;

@JsonDeserialize(using = PostAggregationCountResponse.Deserializer.class)
Expand Down Expand Up @@ -84,13 +85,27 @@ static final class Deserializer extends StdDeserializer<PostAggregationCountResp
public PostAggregationCountResponse deserialize(JsonParser p, DeserializationContext context)
throws IOException {
Object value = p.readValueAs(Object.class);
try {
return of(ObjectMappers.JSON_MAPPER.convertValue(value, AggregationCountResponseDto.class));
} catch (RuntimeException e) {
if (value instanceof Map<?, ?>
&& ((Map<?, ?>) value).containsKey("status")
&& ((Map<?, ?>) value).containsKey("total_hits")
&& ((Map<?, ?>) value).containsKey("page")
&& ((Map<?, ?>) value).containsKey("total_pages")
&& ((Map<?, ?>) value).containsKey("page_size")) {
try {
return of(ObjectMappers.JSON_MAPPER.convertValue(value, AggregationCountResponseDto.class));
} catch (RuntimeException e) {
}
}
try {
return of(ObjectMappers.JSON_MAPPER.convertValue(value, FailedAggregationCountResponseDto.class));
} catch (RuntimeException e) {
if (value instanceof Map<?, ?>
&& ((Map<?, ?>) value).containsKey("status")
&& ((Map<?, ?>) value).containsKey("total_hits")
&& ((Map<?, ?>) value).containsKey("page")
&& ((Map<?, ?>) value).containsKey("total_pages")
&& ((Map<?, ?>) value).containsKey("page_size")) {
try {
return of(ObjectMappers.JSON_MAPPER.convertValue(value, FailedAggregationCountResponseDto.class));
} catch (RuntimeException e) {
}
}
throw new JsonParseException(p, "Failed to deserialize");
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
import com.newscatcher.api.types.FailedAuthorsResponseDto;
import com.newscatcher.api.types.SearchResponseDto;
import java.io.IOException;
import java.util.Map;
import java.util.Objects;

@JsonDeserialize(using = GetAuthorsResponse.Deserializer.class)
Expand Down Expand Up @@ -83,13 +84,27 @@ static final class Deserializer extends StdDeserializer<GetAuthorsResponse> {
@java.lang.Override
public GetAuthorsResponse deserialize(JsonParser p, DeserializationContext context) throws IOException {
Object value = p.readValueAs(Object.class);
try {
return of(ObjectMappers.JSON_MAPPER.convertValue(value, SearchResponseDto.class));
} catch (RuntimeException e) {
if (value instanceof Map<?, ?>
&& ((Map<?, ?>) value).containsKey("status")
&& ((Map<?, ?>) value).containsKey("total_hits")
&& ((Map<?, ?>) value).containsKey("page")
&& ((Map<?, ?>) value).containsKey("total_pages")
&& ((Map<?, ?>) value).containsKey("page_size")) {
try {
return of(ObjectMappers.JSON_MAPPER.convertValue(value, SearchResponseDto.class));
} catch (RuntimeException e) {
}
}
try {
return of(ObjectMappers.JSON_MAPPER.convertValue(value, FailedAuthorsResponseDto.class));
} catch (RuntimeException e) {
if (value instanceof Map<?, ?>
&& ((Map<?, ?>) value).containsKey("status")
&& ((Map<?, ?>) value).containsKey("total_hits")
&& ((Map<?, ?>) value).containsKey("page")
&& ((Map<?, ?>) value).containsKey("total_pages")
&& ((Map<?, ?>) value).containsKey("page_size")) {
try {
return of(ObjectMappers.JSON_MAPPER.convertValue(value, FailedAuthorsResponseDto.class));
} catch (RuntimeException e) {
}
}
throw new JsonParseException(p, "Failed to deserialize");
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
import com.newscatcher.api.types.FailedAuthorsResponseDto;
import com.newscatcher.api.types.SearchResponseDto;
import java.io.IOException;
import java.util.Map;
import java.util.Objects;

@JsonDeserialize(using = PostAuthorsResponse.Deserializer.class)
Expand Down Expand Up @@ -83,13 +84,27 @@ static final class Deserializer extends StdDeserializer<PostAuthorsResponse> {
@java.lang.Override
public PostAuthorsResponse deserialize(JsonParser p, DeserializationContext context) throws IOException {
Object value = p.readValueAs(Object.class);
try {
return of(ObjectMappers.JSON_MAPPER.convertValue(value, SearchResponseDto.class));
} catch (RuntimeException e) {
if (value instanceof Map<?, ?>
&& ((Map<?, ?>) value).containsKey("status")
&& ((Map<?, ?>) value).containsKey("total_hits")
&& ((Map<?, ?>) value).containsKey("page")
&& ((Map<?, ?>) value).containsKey("total_pages")
&& ((Map<?, ?>) value).containsKey("page_size")) {
try {
return of(ObjectMappers.JSON_MAPPER.convertValue(value, SearchResponseDto.class));
} catch (RuntimeException e) {
}
}
try {
return of(ObjectMappers.JSON_MAPPER.convertValue(value, FailedAuthorsResponseDto.class));
} catch (RuntimeException e) {
if (value instanceof Map<?, ?>
&& ((Map<?, ?>) value).containsKey("status")
&& ((Map<?, ?>) value).containsKey("total_hits")
&& ((Map<?, ?>) value).containsKey("page")
&& ((Map<?, ?>) value).containsKey("total_pages")
&& ((Map<?, ?>) value).containsKey("page_size")) {
try {
return of(ObjectMappers.JSON_MAPPER.convertValue(value, FailedAuthorsResponseDto.class));
} catch (RuntimeException e) {
}
}
throw new JsonParseException(p, "Failed to deserialize");
}
Expand Down
Loading
Loading