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
2 changes: 1 addition & 1 deletion .release-please-manifest.json
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
{
".": "0.51.0"
".": "0.52.0"
}
2 changes: 2 additions & 0 deletions .stats.yml
Original file line number Diff line number Diff line change
@@ -1,2 +1,4 @@
configured_endpoints: 103
openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/orb%2Forb-36a6db97756e8658369c9af3c0ac532ecacb032e5b8f6211094dcb4052943ff3.yml
openapi_spec_hash: 26886fa8e59fa3674320897e3409e540
config_hash: ec4f1e02d3528e3a93a73e33bca17c2a
30 changes: 30 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,35 @@
# Changelog

## 0.52.0 (2025-03-26)

Full Changelog: [v0.51.0...v0.52.0](https://github.com/orbcorp/orb-java/compare/v0.51.0...v0.52.0)

### Features

* **client:** support a lower jackson version ([#368](https://github.com/orbcorp/orb-java/issues/368)) ([659e62b](https://github.com/orbcorp/orb-java/commit/659e62bfba72b4fbf261db18cbcc4ced30f7953d))
* **client:** throw on incompatible jackson version ([659e62b](https://github.com/orbcorp/orb-java/commit/659e62bfba72b4fbf261db18cbcc4ced30f7953d))


### Bug Fixes

* **client:** map deserialization bug ([da01e21](https://github.com/orbcorp/orb-java/commit/da01e21aab23d7a6b429cbc1c96f48354ca2f0ca))


### Chores

* **internal:** delete unused methods and annotations ([#369](https://github.com/orbcorp/orb-java/issues/369)) ([da01e21](https://github.com/orbcorp/orb-java/commit/da01e21aab23d7a6b429cbc1c96f48354ca2f0ca))
* **internal:** fix example formatting ([#364](https://github.com/orbcorp/orb-java/issues/364)) ([99e8b5b](https://github.com/orbcorp/orb-java/commit/99e8b5b46b1cba17b06d5fa92fcf8e084d64f6b3))
* **internal:** make multipart assertions more robust ([4f4527e](https://github.com/orbcorp/orb-java/commit/4f4527eaa976d407948d20998b46bc1489c86b2c))
* **internal:** remove unnecessary `assertNotNull` calls ([4f4527e](https://github.com/orbcorp/orb-java/commit/4f4527eaa976d407948d20998b46bc1489c86b2c))
* **internal:** remove unnecessary import ([#365](https://github.com/orbcorp/orb-java/issues/365)) ([3a0fe1e](https://github.com/orbcorp/orb-java/commit/3a0fe1e9bf719ef3606c30ff48ba6a7dd06f7911))


### Documentation

* minor readme tweak ([#367](https://github.com/orbcorp/orb-java/issues/367)) ([bc3f6c3](https://github.com/orbcorp/orb-java/commit/bc3f6c3c810124041192a841b531039b27cfb840))
* refine comments on multipart params ([#362](https://github.com/orbcorp/orb-java/issues/362)) ([4f4527e](https://github.com/orbcorp/orb-java/commit/4f4527eaa976d407948d20998b46bc1489c86b2c))
* update readme exception docs ([#366](https://github.com/orbcorp/orb-java/issues/366)) ([9443b23](https://github.com/orbcorp/orb-java/commit/9443b239a271eb97bf16f5d8cd432f6c97db2aa4))

## 0.51.0 (2025-03-20)

Full Changelog: [v0.50.0...v0.51.0](https://github.com/orbcorp/orb-java/compare/v0.50.0...v0.51.0)
Expand Down
32 changes: 16 additions & 16 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,11 @@

<!-- x-release-please-start-version -->

[![Maven Central](https://img.shields.io/maven-central/v/com.withorb.api/orb-java)](https://central.sonatype.com/artifact/com.withorb.api/orb-java/0.51.0)
[![Maven Central](https://img.shields.io/maven-central/v/com.withorb.api/orb-java)](https://central.sonatype.com/artifact/com.withorb.api/orb-java/0.52.0)

<!-- x-release-please-end -->

The Orb Java SDK provides convenient access to the Orb REST API from applications written in Java.
The Orb Java SDK provides convenient access to the [Orb REST API](https://docs.withorb.com/reference/api-reference) from applications written in Java.

The Orb Java SDK is similar to the Orb Kotlin SDK but with minor differences that make it more ergonomic for use in Java, such as `Optional` instead of nullable values, `Stream` instead of `Sequence`, and `CompletableFuture` instead of suspend functions.

Expand All @@ -19,16 +19,16 @@ The REST API documentation can be found on [docs.withorb.com](https://docs.witho
### Gradle

```kotlin
implementation("com.withorb.api:orb-java:0.51.0")
implementation("com.withorb.api:orb-java:0.52.0")
```

### Maven

```xml
<dependency>
<groupId>com.withorb.api</groupId>
<artifactId>orb-java</artifactId>
<version>0.51.0</version>
<groupId>com.withorb.api</groupId>
<artifactId>orb-java</artifactId>
<version>0.52.0</version>
</dependency>
```

Expand Down Expand Up @@ -195,16 +195,16 @@ The SDK throws custom unchecked exception types:

- [`OrbServiceException`](orb-java-core/src/main/kotlin/com/withorb/api/errors/OrbServiceException.kt): Base class for HTTP errors. See this table for which exception subclass is thrown for each HTTP status code:

| Status | Exception |
| ------ | ------------------------------- |
| 400 | `BadRequestException` |
| 401 | `AuthenticationException` |
| 403 | `PermissionDeniedException` |
| 404 | `NotFoundException` |
| 422 | `UnprocessableEntityException` |
| 429 | `RateLimitException` |
| 5xx | `InternalServerException` |
| others | `UnexpectedStatusCodeException` |
| Status | Exception |
| ------ | ------------------------------------------------------------------------------------------------------------------------ |
| 400 | [`BadRequestException`](orb-java-core/src/main/kotlin/com/withorb/api/errors/BadRequestException.kt) |
| 401 | [`UnauthorizedException`](orb-java-core/src/main/kotlin/com/withorb/api/errors/UnauthorizedException.kt) |
| 403 | [`PermissionDeniedException`](orb-java-core/src/main/kotlin/com/withorb/api/errors/PermissionDeniedException.kt) |
| 404 | [`NotFoundException`](orb-java-core/src/main/kotlin/com/withorb/api/errors/NotFoundException.kt) |
| 422 | [`UnprocessableEntityException`](orb-java-core/src/main/kotlin/com/withorb/api/errors/UnprocessableEntityException.kt) |
| 429 | [`RateLimitException`](orb-java-core/src/main/kotlin/com/withorb/api/errors/RateLimitException.kt) |
| 5xx | [`InternalServerException`](orb-java-core/src/main/kotlin/com/withorb/api/errors/InternalServerException.kt) |
| others | [`UnexpectedStatusCodeException`](orb-java-core/src/main/kotlin/com/withorb/api/errors/UnexpectedStatusCodeException.kt) |

- [`OrbIoException`](orb-java-core/src/main/kotlin/com/withorb/api/errors/OrbIoException.kt): I/O networking errors.

Expand Down
2 changes: 1 addition & 1 deletion build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
allprojects {
group = "com.withorb.api"
version = "0.51.0" // x-release-please-version
version = "0.52.0" // x-release-please-version
}
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,17 @@ class OrbOkHttpClient private constructor() {
this.baseUrl = baseUrl
}

/**
* Whether to throw an exception if any of the Jackson versions detected at runtime are
* incompatible with the SDK's minimum supported Jackson version (2.13.4).
*
* Defaults to true. Use extreme caution when disabling this option. There is no guarantee
* that the SDK will work correctly when using an incompatible Jackson version.
*/
fun checkJacksonVersionCompatibility(checkJacksonVersionCompatibility: Boolean) = apply {
clientOptions.checkJacksonVersionCompatibility(checkJacksonVersionCompatibility)
}

fun jsonMapper(jsonMapper: JsonMapper) = apply { clientOptions.jsonMapper(jsonMapper) }

fun clock(clock: Clock) = apply { clientOptions.clock(clock) }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,17 @@ class OrbOkHttpClientAsync private constructor() {
this.baseUrl = baseUrl
}

/**
* Whether to throw an exception if any of the Jackson versions detected at runtime are
* incompatible with the SDK's minimum supported Jackson version (2.13.4).
*
* Defaults to true. Use extreme caution when disabling this option. There is no guarantee
* that the SDK will work correctly when using an incompatible Jackson version.
*/
fun checkJacksonVersionCompatibility(checkJacksonVersionCompatibility: Boolean) = apply {
clientOptions.checkJacksonVersionCompatibility(checkJacksonVersionCompatibility)
}

fun jsonMapper(jsonMapper: JsonMapper) = apply { clientOptions.jsonMapper(jsonMapper) }

fun clock(clock: Clock) = apply { clientOptions.clock(clock) }
Expand Down
13 changes: 13 additions & 0 deletions orb-java-core/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,19 @@ plugins {
id("orb.publish")
}

configurations.all {
resolutionStrategy {
// Compile and test against a lower Jackson version to ensure we're compatible with it.
// We publish with a higher version (see below) to ensure users depend on a secure version by default.
force("com.fasterxml.jackson.core:jackson-core:2.13.4")
force("com.fasterxml.jackson.core:jackson-databind:2.13.4")
force("com.fasterxml.jackson.core:jackson-annotations:2.13.4")
force("com.fasterxml.jackson.datatype:jackson-datatype-jdk8:2.13.4")
force("com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.13.4")
force("com.fasterxml.jackson.module:jackson-module-kotlin:2.13.4")
}
}

dependencies {
api("com.fasterxml.jackson.core:jackson-core:2.18.1")
api("com.fasterxml.jackson.core:jackson-databind:2.18.1")
Expand Down
46 changes: 46 additions & 0 deletions orb-java-core/src/main/kotlin/com/withorb/api/core/Check.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@

package com.withorb.api.core

import com.fasterxml.jackson.core.Version
import com.fasterxml.jackson.core.util.VersionUtil

fun <T : Any> checkRequired(name: String, value: T?): T =
checkNotNull(value) { "`$name` is required, but was not set" }

Expand Down Expand Up @@ -39,3 +42,46 @@ internal fun checkMaxLength(name: String, value: String, maxLength: Int): String
"`$name` must have at most length $maxLength, but was ${it.length}"
}
}

@JvmSynthetic
internal fun checkJacksonVersionCompatibility() {
val incompatibleJacksonVersions =
RUNTIME_JACKSON_VERSIONS.mapNotNull {
when {
it.majorVersion != MINIMUM_JACKSON_VERSION.majorVersion ->
it to "incompatible major version"
it.minorVersion < MINIMUM_JACKSON_VERSION.minorVersion ->
it to "minor version too low"
it.minorVersion == MINIMUM_JACKSON_VERSION.minorVersion &&
it.patchLevel < MINIMUM_JACKSON_VERSION.patchLevel ->
it to "patch version too low"
else -> null
}
}
check(incompatibleJacksonVersions.isEmpty()) {
"""
This SDK depends on Jackson version $MINIMUM_JACKSON_VERSION, but the following incompatible Jackson versions were detected at runtime:

${incompatibleJacksonVersions.asSequence().map { (version, incompatibilityReason) ->
"- `${version.toFullString().replace("/", ":")}` ($incompatibilityReason)"
}.joinToString("\n")}

This can happen if you are either:
1. Directly depending on different Jackson versions
2. Depending on some library that depends on different Jackson versions, potentially transitively

Double-check that you are depending on compatible Jackson versions.
"""
.trimIndent()
}
}

private val MINIMUM_JACKSON_VERSION: Version = VersionUtil.parseVersion("2.13.4", null, null)
private val RUNTIME_JACKSON_VERSIONS: List<Version> =
listOf(
com.fasterxml.jackson.core.json.PackageVersion.VERSION,
com.fasterxml.jackson.databind.cfg.PackageVersion.VERSION,
com.fasterxml.jackson.datatype.jdk8.PackageVersion.VERSION,
com.fasterxml.jackson.datatype.jsr310.PackageVersion.VERSION,
com.fasterxml.jackson.module.kotlin.PackageVersion.VERSION,
)
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ class ClientOptions
private constructor(
private val originalHttpClient: HttpClient,
@get:JvmName("httpClient") val httpClient: HttpClient,
@get:JvmName("checkJacksonVersionCompatibility") val checkJacksonVersionCompatibility: Boolean,
@get:JvmName("jsonMapper") val jsonMapper: JsonMapper,
@get:JvmName("clock") val clock: Clock,
@get:JvmName("baseUrl") val baseUrl: String,
Expand All @@ -28,6 +29,12 @@ private constructor(
private val webhookSecret: String?,
) {

init {
if (checkJacksonVersionCompatibility) {
checkJacksonVersionCompatibility()
}
}

fun webhookSecret(): Optional<String> = Optional.ofNullable(webhookSecret)

fun toBuilder() = Builder().from(this)
Expand All @@ -54,6 +61,7 @@ private constructor(
class Builder internal constructor() {

private var httpClient: HttpClient? = null
private var checkJacksonVersionCompatibility: Boolean = true
private var jsonMapper: JsonMapper = jsonMapper()
private var clock: Clock = Clock.systemUTC()
private var baseUrl: String = PRODUCTION_URL
Expand All @@ -68,6 +76,7 @@ private constructor(
@JvmSynthetic
internal fun from(clientOptions: ClientOptions) = apply {
httpClient = clientOptions.originalHttpClient
checkJacksonVersionCompatibility = clientOptions.checkJacksonVersionCompatibility
jsonMapper = clientOptions.jsonMapper
clock = clientOptions.clock
baseUrl = clientOptions.baseUrl
Expand All @@ -82,6 +91,10 @@ private constructor(

fun httpClient(httpClient: HttpClient) = apply { this.httpClient = httpClient }

fun checkJacksonVersionCompatibility(checkJacksonVersionCompatibility: Boolean) = apply {
this.checkJacksonVersionCompatibility = checkJacksonVersionCompatibility
}

fun jsonMapper(jsonMapper: JsonMapper) = apply { this.jsonMapper = jsonMapper }

fun clock(clock: Clock) = apply { this.clock = clock }
Expand Down Expand Up @@ -233,6 +246,7 @@ private constructor(
.idempotencyHeader("Idempotency-Key")
.build()
),
checkJacksonVersionCompatibility,
jsonMapper,
clock,
baseUrl,
Expand Down
54 changes: 10 additions & 44 deletions orb-java-core/src/main/kotlin/com/withorb/api/core/ObjectMappers.kt
Original file line number Diff line number Diff line change
Expand Up @@ -5,23 +5,19 @@ package com.withorb.api.core
import com.fasterxml.jackson.annotation.JsonInclude
import com.fasterxml.jackson.core.JsonGenerator
import com.fasterxml.jackson.databind.DeserializationFeature
import com.fasterxml.jackson.databind.MapperFeature
import com.fasterxml.jackson.databind.SerializationFeature
import com.fasterxml.jackson.databind.SerializerProvider
import com.fasterxml.jackson.databind.cfg.CoercionAction.Fail
import com.fasterxml.jackson.databind.cfg.CoercionInputShape.Integer
import com.fasterxml.jackson.databind.exc.InvalidDefinitionException
import com.fasterxml.jackson.databind.exc.UnrecognizedPropertyException
import com.fasterxml.jackson.databind.json.JsonMapper
import com.fasterxml.jackson.databind.module.SimpleModule
import com.fasterxml.jackson.datatype.jdk8.Jdk8Module
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule
import com.fasterxml.jackson.module.kotlin.jacksonMapperBuilder
import com.withorb.api.errors.OrbException
import com.withorb.api.errors.OrbInvalidDataException
import com.fasterxml.jackson.module.kotlin.kotlinModule
import java.io.InputStream

fun jsonMapper(): JsonMapper =
jacksonMapperBuilder()
JsonMapper.builder()
.addModule(kotlinModule())
.addModule(Jdk8Module())
.addModule(JavaTimeModule())
.addModule(SimpleModule().addSerializer(InputStreamJsonSerializer))
Expand All @@ -30,7 +26,12 @@ fun jsonMapper(): JsonMapper =
.disable(SerializationFeature.FLUSH_AFTER_WRITE_VALUE)
.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)
.disable(SerializationFeature.WRITE_DURATIONS_AS_TIMESTAMPS)
.withCoercionConfig(String::class.java) { it.setCoercion(Integer, Fail) }
.disable(MapperFeature.ALLOW_COERCION_OF_SCALARS)
.disable(MapperFeature.AUTO_DETECT_CREATORS)
.disable(MapperFeature.AUTO_DETECT_FIELDS)
.disable(MapperFeature.AUTO_DETECT_GETTERS)
.disable(MapperFeature.AUTO_DETECT_IS_GETTERS)
.disable(MapperFeature.AUTO_DETECT_SETTERS)
.build()

private object InputStreamJsonSerializer : BaseSerializer<InputStream>(InputStream::class) {
Expand All @@ -47,38 +48,3 @@ private object InputStreamJsonSerializer : BaseSerializer<InputStream>(InputStre
}
}
}

@JvmSynthetic
internal fun enhanceJacksonException(fallbackMessage: String, e: Exception): Exception {
// These exceptions should only happen if our code is wrong OR if the user is using a binary
// incompatible version of `com.fasterxml.jackson.core:jackson-databind`:
// https://javadoc.io/static/com.fasterxml.jackson.core/jackson-databind/2.18.1/index.html
val isUnexpectedException =
e is UnrecognizedPropertyException || e is InvalidDefinitionException
if (!isUnexpectedException) {
return OrbInvalidDataException(fallbackMessage, e)
}

val jacksonVersion = JsonMapper::class.java.`package`.implementationVersion
if (jacksonVersion.isNullOrEmpty() || jacksonVersion == COMPILED_JACKSON_VERSION) {
return OrbInvalidDataException(fallbackMessage, e)
}

return OrbException(
"""
Jackson threw an unexpected exception and its runtime version ($jacksonVersion) mismatches the version the SDK was compiled with ($COMPILED_JACKSON_VERSION).

You may be using a version of `com.fasterxml.jackson.core:jackson-databind` that's not binary compatible with the SDK.

This can happen if you are either:
1. Directly depending on a different Jackson version
2. Depending on some library that depends on a different Jackson version, potentially transitively

Double-check that you are depending on a compatible Jackson version.
"""
.trimIndent(),
e,
)
}

const val COMPILED_JACKSON_VERSION = "2.18.1"
Loading