---
title: "String operations"
description: "Inspect and transform string bins server-side with the Aerospike Developer SDK's BinBuilder and StringExp APIs, using Java and Python."
---

# String operations

> For the complete documentation index see: [llms.txt](https://aerospike.com/docs/llms.txt)
> 
> All documentation pages available in markdown.

## Applies to

-   Aerospike Developer SDK (Java 21+ and Python 3.11+)
-   Aerospike Database 6.0 or later unless a section states otherwise

::: code examples
Examples assume a connected `session` from [Connect to Aerospike](https://aerospike.com/docs/develop/client/sdk/connect). Each usage guide lists shared imports in a tabbed block near the top of the page; snippet blocks add imports only for types not already shown. The first code block in each section includes dataset setup; later blocks in that section reuse those values. When a page includes a **Complete example** section, that block is fully self-contained.
:::

Learn how to inspect and transform string bins on the server, without reading the whole value to the client first. This guide covers reading substrings and search results, and modifying strings in place (case conversion, trimming, padding, regex replace, and more). It also covers converting between string and other types, and applying the same operations inside filter and projection expressions.

::: version requirement
String read/modify operations require Aerospike Database 8.2.0 or later. Earlier server versions reject these operations. During a rolling upgrade, don’t issue these operations until every node in the cluster reports Database 8.2.0 or later. A partially upgraded cluster accepts these operations on upgraded nodes, and rejects the same operation on nodes that aren’t upgraded yet. Check your SDK release notes for the minimum client version. See [Version Compatibility](https://aerospike.com/docs/develop/client/sdk/reference/compatibility).
:::

Except where noted, snippets on this page use the imports below. A snippet lists additional `import` lines only when it needs a type not shown here. When this page includes a **Complete example** section, that block is fully self-contained with every import required to run it.

-   [Java](#tab-panel-6118)
-   [Python](#tab-panel-6119)

```java
import com.aerospike.client.sdk.DataSet;

import com.aerospike.client.sdk.Record;

import com.aerospike.client.sdk.RecordStream;

import com.aerospike.client.sdk.StringWriteOptions;

import com.aerospike.client.sdk.exp.Exp;

import com.aerospike.client.sdk.exp.StringExp;
```

```python
from aerospike_sdk import DataSet, Exp, StringWriteFlags
```

## Two ways to work with strings

| Surface | Role |
| --- | --- |
| `BinBuilder` (`session.query(key).bin("s").<op>()` / `session.upsert(key).bin("s").<op>()`) | Fluent chain. Each method queues one server-side string operation on the named bin. |
| `StringExp` (Java) / `Exp.string_*` (Python) | Expression builders for filters (`.where(...)`), `selectFrom`/`select_from` projections, and composing reads. Modify-style expressions return the transformed value, and don’t write it back to the bin. |

::: append() / prepend() and the string-op forms
Both SDKs already had bin-level `append(...)` / `prepend(...)` (see [Append to strings](https://aerospike.com/docs/develop/client/sdk/usage/update/#append-to-strings)). On a Database 8.2.0 cluster, Java’s `BinBuilder.append(...)` / `.prepend(...)` are sent as the string `append` / `prepend` operations and accept `StringWriteOptions`. On an older cluster they fall back to the classic bin append/prepend. Python keeps `.append(...)` / `.prepend(...)` as the classic operations (no flags, no version floor) and adds `str_append(...)` / `str_prepend(...)` for the 8.2.0 forms that take `flags=`.
:::

## Multiple operations on one bin

When a query or write queues several operations against the _same_ bin, as in the examples throughout this page, the results come back in submission order, one slot per operation. Modify operations contribute a `null`/`None` slot: the server returns no value for them. Chain a trailing `.get()` on the bin when you want the value after the modifications.

-   [Java](#tab-panel-6120)
-   [Python](#tab-panel-6121)

Use [`Record.operationResult(index)`](https://javadoc.io/doc/com.aerospike/aerospike-client-sdk/latest/com/aerospike/client/sdk/Record.html#operationResult%28int%29) with the zero-based position of the operation in the call chain, then a typed getter such as `getLong()` or `getString()`. This is distinct from `record.bins.get(name)`, which reads a single bin’s current value (see [Update records](https://aerospike.com/docs/develop/client/sdk/usage/update/#increment-numbers) for that pattern with numeric bins).

Use `RecordResult.operation_result(index)` with the zero-based position of the operation in the call chain. The accessor is on the `RecordResult` row returned by `first_or_raise()`. By-name access through `record.bins[name]` holds a list when several operations target one bin, a single value when only one operation returned a value for that bin, and no entry at all when every operation on the bin was a modify.

## Read operations

Indexes are Unicode codepoints, left to right. Negative indexes count from the end (`-1` is the last codepoint). Out-of-range indexes are clamped and do not throw an error.

For example, for the string “hello”, `substr(3, 100)` / `str_substr(3, 100)` returns “lo”.

| Java | Python | Returns |
| --- | --- | --- |
| `strlen()` | `str_strlen()` | Codepoint count (int64) |
| `substr(start[, end])` | `str_substr(start[, end])` | Substring, half-open `[start, end)` when `end` is given, otherwise through the end of the string |
| `charAt(index)` | `str_char_at(index)` | Single-codepoint string at `index` |
| `find(needle[, occurrence])` | `str_find(needle[, occurrence])` | Codepoint index of the match, or `-1`. `occurrence` is 1-based (`1` = first, `-1` = last), not a start offset. Defaults to `1` |
| `contains(needle)` | `str_contains(needle)` | Boolean |
| `startsWith(prefix)` | `str_starts_with(prefix)` | Boolean |
| `endsWith(suffix)` | `str_ends_with(suffix)` | Boolean |
| `byteLength()` | `str_byte_length()` | UTF-8 byte length |
| `isNumeric([numericType])` | `str_is_numeric([numeric_type])` | Boolean, optionally restricted to `INT` or `FLOAT` (see [Type conversion](#type-conversion)) |
| `isUpper()` / `isLower()` | `str_is_upper()` / `str_is_lower()` | Boolean |
| `split([separator])` | `str_split([separator])` | List of strings. Omitted separator splits per codepoint |
| `b64Decode()` | `str_b64_decode()` | Blob decoded from base64 |
| `regexCompare(pattern[, regexFlags])` | `str_regex_compare(pattern[, flags])` | Boolean match result (see [Regex flags](#regex-flags)) |

::: codepoints are not user-perceived characters
`strlen()` / `str_strlen()` counts Unicode codepoints, matching `String#codePointCount` in Java. This agrees with visible character count for ASCII and basic Latin text. It diverges for combining marks, emoji modifiers, and zero-width-joiner sequences: for example, a family emoji built from four ZWJ-joined codepoints counts as 4, not 1. Codepoint count is also not the same as UTF-16 code units (Java’s `String#length()`) or UTF-8 byte length. Use `byteLength()` / `str_byte_length()` for byte length.
:::

-   [Java](#tab-panel-6122)
-   [Python](#tab-panel-6123)

```java
DataSet docs = DataSet.of("test", "docs");

String key = "row1";

session.upsert(docs.id(key)).bin("message").setTo("hello world").execute();

RecordStream stream = session.query(docs.id(key))

    .bin("message").strlen()

    .bin("message").substr(6)

    .bin("message").substr(0, 5)

    .bin("message").find("o", -1)

    .bin("message").contains("world")

    .execute();

Record rec = stream.getFirst().orElseThrow().recordOrThrow();

System.out.println("length: " + rec.operationResult(0).getLong());

System.out.println("substr(6): " + rec.operationResult(1).getString());

System.out.println("substr(0,5): " + rec.operationResult(2).getString());

System.out.println("last 'o' at: " + rec.operationResult(3).getLong());

System.out.println("contains 'world': " + rec.operationResult(4).getBoolean());

stream.close();
```

> 📖 **API reference**: [`DataSet.of(...)`](https://javadoc.io/doc/com.aerospike/aerospike-client-sdk/latest/com/aerospike/client/sdk/DataSet.html#of%28java.lang.String%2Cjava.lang.String%29) | [`DataSet.id(...)`](https://javadoc.io/doc/com.aerospike/aerospike-client-sdk/latest/com/aerospike/client/sdk/DataSet.html#id%28java.lang.String%29) | [`Session.query(Key)`](https://javadoc.io/doc/com.aerospike/aerospike-client-sdk/latest/com/aerospike/client/sdk/Session.html#query%28com.aerospike.client.sdk.Key%29) | [`ChainableQueryBuilder.execute()`](https://javadoc.io/doc/com.aerospike/aerospike-client-sdk/latest/com/aerospike/client/sdk/ChainableQueryBuilder.html#execute%28%29) | [`RecordStream.getFirst()`](https://javadoc.io/doc/com.aerospike/aerospike-client-sdk/latest/com/aerospike/client/sdk/RecordStream.html#getFirst%28%29) | [`RecordStream.close()`](https://javadoc.io/doc/com.aerospike/aerospike-client-sdk/latest/com/aerospike/client/sdk/RecordStream.html#close%28%29) | [`RecordResult.recordOrThrow()`](https://javadoc.io/doc/com.aerospike/aerospike-client-sdk/latest/com/aerospike/client/sdk/RecordResult.html#recordOrThrow%28%29) | [`Record.operationResult(...)`](https://javadoc.io/doc/com.aerospike/aerospike-client-sdk/latest/com/aerospike/client/sdk/Record.html#operationResult%28int%29)

```python
docs = DataSet.of("test", "docs")

key = docs.id("row1")

await session.upsert(key).put({"message": "hello world"}).execute()

stream = await (

    session.query(key)

    .bin("message").str_strlen()

    .bin("message").str_substr(6)

    .bin("message").str_substr(0, 5)

    .bin("message").str_find("o", -1)

    .bin("message").str_contains("world")

    .execute()

)

row = await stream.first_or_raise()

print(f"length: {row.operation_result(0)}")

print(f"substr(6): {row.operation_result(1)}")

print(f"substr(0,5): {row.operation_result(2)}")

print(f"last 'o' at: {row.operation_result(3)}")

print(f"contains 'world': {row.operation_result(4)}")

stream.close()
```

> 📖 **API reference**: [`DataSet.of()`](https://aerospike-python-sdk.readthedocs.io/en/latest/api/dataset.html#aerospike%5Fsdk.dataset.DataSet.of) | [`DataSet.id()`](https://aerospike-python-sdk.readthedocs.io/en/latest/api/dataset.html#aerospike%5Fsdk.dataset.DataSet.id) | [`Session.query()`](https://aerospike-python-sdk.readthedocs.io/en/latest/api/session.html#aerospike%5Fsdk.aio.session.Session.query) | [`Session.upsert()`](https://aerospike-python-sdk.readthedocs.io/en/latest/api/session.html#aerospike%5Fsdk.aio.session.Session.upsert) | [`WriteSegmentBuilder.put()`](https://aerospike-python-sdk.readthedocs.io/en/latest/api/write-segment.html#aerospike%5Fsdk.aio.operations.query.WriteSegmentBuilder.put) | [`RecordResult.operation_result()`](https://aerospike-python-sdk.readthedocs.io/en/latest/api/record-result.html#aerospike%5Fsdk.record%5Fresult.RecordResult.operation%5Fresult) | [`RecordStream.first_or_raise()`](https://aerospike-python-sdk.readthedocs.io/en/latest/api/record-stream.html#aerospike%5Fsdk.record%5Fstream.RecordStream.first%5For%5Fraise) | [`RecordStream.close()`](https://aerospike-python-sdk.readthedocs.io/en/latest/api/record-stream.html#aerospike%5Fsdk.record%5Fstream.RecordStream.close)

## Modify operations

Modify operations change the bin in place. They return no value: each one contributes a `null`/`None` slot to the positional results, so chain a trailing `.get()` on the bin to read the value after the modifications. All modify operations accept optional write flags. See [String write flags](#string-write-flags).

| Java | Python | Effect |
| --- | --- | --- |
| `insert(index, value[, options])` | `str_insert(index, value[, flags=])` | Splice `value` into the bin at codepoint `index` |
| `overwrite(index, value[, options])` | `str_overwrite(index, value[, flags=])` | Overwrite codepoints starting at `index`. Result can grow past the original length |
| `concat(fragment or fragments[, options])` | `str_concat(value[, flags=])` | Append one fragment or, given an ordered list, append each fragment in order |
| `append(fragment[, options])` | `str_append(value[, flags=])` | Append one fragment |
| `prepend(fragment[, options])` | `str_prepend(value[, flags=])` | Prepend one fragment |
| `snip(start[, end][, options])` | `str_snip(start[, end][, flags=])` | Remove codepoints from `start` through `end` (exclusive), or through the end of the string if `end` is omitted |
| `replace(needle, replacement[, options])` | `str_replace(needle, replacement[, flags=])` | Replace the first occurrence of `needle` |
| `replaceAll(needle, replacement[, options])` | `str_replace_all(needle, replacement[, flags=])` | Replace every occurrence of `needle` |
| `upper([options])` / `lower([options])` | `str_upper([flags=])` / `str_lower([flags=])` | Uppercase / lowercase the bin |
| `caseFold([options])` | `str_case_fold([flags=])` | Locale-independent case fold (for normalized comparison keys) |
| `normalizeNfc([options])` | `str_normalize_nfc([flags=])` | Normalize to Unicode Normalization Form C (NFC) |
| `trimStart()` / `trimEnd()` / `trim()` | `str_trim_start()` / `str_trim_end()` / `str_trim()` | Remove Unicode whitespace from one or both ends. Each accepts `[options]` / `[flags=]` |
| `padStart(targetLength, padString[, options])` | `str_pad_start(target_length, pad_string[, flags=])` | Left-pad to a minimum length |
| `padEnd(targetLength, padString[, options])` | `str_pad_end(target_length, pad_string[, flags=])` | Right-pad to a minimum length |
| `repeat(count[, options])` | `str_repeat(count[, flags=])` | Repeat the bin’s contents `count` times |
| `regexReplace(pattern, replacement[, regexFlags][, options])` | `str_regex_replace(pattern, replacement[, flags][, write_flags=])` | Regex-based replace. See [Regex flags](#regex-flags) |

::: unbounded growth can exceed the record size limit
`repeat()`, `padStart()`/`padEnd()`, `concat()`/`str_concat()`, `insert()`, and `overwrite()` can all grow the bin without a stated limit. A large `count` or `targetLength` derived from unvalidated input can push the record past the namespace’s maximum record or bin size, which fails the write with `RecordTooBigException` / `RecordTooBigError` (`RECORD_TOO_BIG`). Validate these inputs before use in production. See [Error handling](https://aerospike.com/docs/develop/client/sdk/concepts/errors) for the full exception hierarchy.
:::
::: regexreplace takes both regex flags and write flags
`regexReplace()` / `str_regex_replace()` is the one modify operation with two flag arguments. In Java, the `int regexFlags` argument precedes the `StringWriteOptions`. In Python, the positional `flags` argument is the `StringRegexFlags` bitmask (unlike the other `str_*` methods, where `flags=` carries write flags), and write flags travel in the `write_flags=` keyword. `CREATE_ONLY` is rejected with `PARAMETER_ERROR` here because a regex replace can’t create a bin. To replace only the first match instead of every match, omit `StringRegexFlags.GLOBAL`.
:::

-   [Java](#tab-panel-6124)
-   [Python](#tab-panel-6125)

```java
DataSet docs = DataSet.of("test", "docs");

String key = "row1";

session.upsert(docs.id(key)).bin("title").setTo("  the Quick Brown Fox  ").execute();

RecordStream stream = session.update(docs.id(key))

    .bin("title").trim()

    .bin("title").upper()

    .bin("title").padEnd(30, ".")

    .bin("title").get()

    .execute();

Record rec = stream.getFirst().orElseThrow().recordOrThrow();

// Slots 0-2 are the modify operations and hold null; slot 3 is the trailing get.

System.out.println("after trim, upper, padEnd: " + rec.operationResult(3).getString());

stream.close();

// CREATE_ONLY plus NO_FAIL: create the bin if it's missing, otherwise leave it

// unchanged instead of failing with BIN_EXISTS_ERROR. "subtitle" doesn't exist

// yet, so the first run creates it and a rerun leaves it alone.

session.update(docs.id(key))

    .bin("subtitle").concat("draft", opt -> opt.createOnly().noFail())

    .execute();
```

> 📖 **API reference**: [`DataSet.of(...)`](https://javadoc.io/doc/com.aerospike/aerospike-client-sdk/latest/com/aerospike/client/sdk/DataSet.html#of%28java.lang.String%2Cjava.lang.String%29) | [`DataSet.id(...)`](https://javadoc.io/doc/com.aerospike/aerospike-client-sdk/latest/com/aerospike/client/sdk/DataSet.html#id%28java.lang.String%29) | [`Session.update(DataSet)`](https://javadoc.io/doc/com.aerospike/aerospike-client-sdk/latest/com/aerospike/client/sdk/Session.html#update%28com.aerospike.client.sdk.DataSet%29) | [`ChainableQueryBuilder.execute()`](https://javadoc.io/doc/com.aerospike/aerospike-client-sdk/latest/com/aerospike/client/sdk/ChainableQueryBuilder.html#execute%28%29) | [`RecordStream.getFirst()`](https://javadoc.io/doc/com.aerospike/aerospike-client-sdk/latest/com/aerospike/client/sdk/RecordStream.html#getFirst%28%29) | [`RecordStream.close()`](https://javadoc.io/doc/com.aerospike/aerospike-client-sdk/latest/com/aerospike/client/sdk/RecordStream.html#close%28%29) | [`RecordResult.recordOrThrow()`](https://javadoc.io/doc/com.aerospike/aerospike-client-sdk/latest/com/aerospike/client/sdk/RecordResult.html#recordOrThrow%28%29) | [`Record.operationResult(...)`](https://javadoc.io/doc/com.aerospike/aerospike-client-sdk/latest/com/aerospike/client/sdk/Record.html#operationResult%28int%29) | [`StringWriteOptions.createOnly()`](https://javadoc.io/doc/com.aerospike/aerospike-client-sdk/latest/com/aerospike/client/sdk/StringWriteOptions.html#createOnly%28%29) | [`StringWriteOptions.noFail()`](https://javadoc.io/doc/com.aerospike/aerospike-client-sdk/latest/com/aerospike/client/sdk/StringWriteOptions.html#noFail%28%29)

```python
docs = DataSet.of("test", "docs")

key = docs.id("row1")

await session.upsert(key).put({"title": "  the Quick Brown Fox  "}).execute()

stream = await (

    session.update(key)

    .bin("title").str_trim()

    .bin("title").str_upper()

    .bin("title").str_pad_end(30, ".")

    .bin("title").get()

    .execute()

)

row = await stream.first_or_raise()

# Slots 0-2 are the modify operations and hold None; slot 3 is the trailing get.

print(f"after trim, upper, pad_end: {row.operation_result(3)}")

stream.close()

# CREATE_ONLY plus NO_FAIL: create the bin if it's missing, otherwise leave it

# unchanged instead of failing with BIN_EXISTS_ERROR. "subtitle" doesn't exist

# yet, so the first run creates it and a rerun leaves it alone.

await (

    session.update(key)

    .bin("subtitle").str_concat("draft", flags=StringWriteFlags.CREATE_ONLY | StringWriteFlags.NO_FAIL)

    .execute()

)
```

> 📖 **API reference**: [`DataSet.of()`](https://aerospike-python-sdk.readthedocs.io/en/latest/api/dataset.html#aerospike%5Fsdk.dataset.DataSet.of) | [`DataSet.id()`](https://aerospike-python-sdk.readthedocs.io/en/latest/api/dataset.html#aerospike%5Fsdk.dataset.DataSet.id) | [`Session.update()`](https://aerospike-python-sdk.readthedocs.io/en/latest/api/session.html#aerospike%5Fsdk.aio.session.Session.update) | [`Session.upsert()`](https://aerospike-python-sdk.readthedocs.io/en/latest/api/session.html#aerospike%5Fsdk.aio.session.Session.upsert) | [`WriteSegmentBuilder.put()`](https://aerospike-python-sdk.readthedocs.io/en/latest/api/write-segment.html#aerospike%5Fsdk.aio.operations.query.WriteSegmentBuilder.put) | [`RecordResult.operation_result()`](https://aerospike-python-sdk.readthedocs.io/en/latest/api/record-result.html#aerospike%5Fsdk.record%5Fresult.RecordResult.operation%5Fresult) | [`RecordStream.first_or_raise()`](https://aerospike-python-sdk.readthedocs.io/en/latest/api/record-stream.html#aerospike%5Fsdk.record%5Fstream.RecordStream.first%5For%5Fraise) | [`RecordStream.close()`](https://aerospike-python-sdk.readthedocs.io/en/latest/api/record-stream.html#aerospike%5Fsdk.record%5Fstream.RecordStream.close)

## String write flags

Java exposes the flags as `StringWriteOptions` builder methods on the fluent API (`opt -> opt.createOnly()`, `opt -> opt.noFail()`) and as `StringWriteFlags` int constants on `StringExp`. Python uses the `StringWriteFlags` enum everywhere. Combine members with bitwise OR.

| Flag | Java | Python | Effect |
| --- | --- | --- | --- |
| Default | `StringWriteFlags.DEFAULT` | `StringWriteFlags.DEFAULT` | On the operations that can create a bin, create it if missing, otherwise update it. On every other operation a missing bin is a silent no-op, not an error |
| Create only | `StringWriteFlags.CREATE_ONLY` / `opt -> opt.createOnly()` | `StringWriteFlags.CREATE_ONLY` | Apply only if the bin doesn’t exist yet. Fails with `BIN_EXISTS_ERROR` on an existing bin. Valid only on operations that can create a bin (`insert`, `overwrite`, `concat`, `append`, `prepend`, `padStart`, `padEnd`, `repeat`), and never with a nested collection data type (CDT) path |
| Update only | `StringWriteFlags.UPDATE_ONLY` / `opt -> opt.updateOnly()` | `StringWriteFlags.UPDATE_ONLY` | Apply only if the bin exists. On a missing bin the operation is a silent no-op and doesn’t create the bin |
| No-fail | `StringWriteFlags.NO_FAIL` / `opt -> opt.noFail()` | `StringWriteFlags.NO_FAIL` | Turn an in-operation failure into a silent no-op: `BIN_EXISTS_ERROR` from `CREATE_ONLY`, or `OP_NOT_APPLICABLE` from a nested path that doesn’t resolve. Leaves the bin unchanged and returns a `null`/`None` slot for that operation |

`CREATE_ONLY` and `UPDATE_ONLY` are mutually exclusive. The server rejects the combination with `PARAMETER_ERROR`.

::: no_fail doesn’t cover type or parameter errors
`NO_FAIL` suppresses failures that arise while applying the string operation itself, including a nested-path resolution failure (`OP_NOT_APPLICABLE`). An absent key or index and a path that navigates into the wrong shape, such as a map-specific selector applied to a list, produce the same error and are both suppressed to a silent no-op. `NO_FAIL` does not suppress a wrong bin type (`BIN_TYPE_ERROR`, for example `upper()` on an integer bin), invalid UTF-8, an invalid flag combination, or `CREATE_ONLY` combined with a CDT path. Those still fail the call. See [Nested strings](#nested-strings) and [Context](https://aerospike.com/docs/develop/data-types/collections/context).
:::

## Regex flags

Used with `regexCompare()`/`str_regex_compare()` and `regexReplace()`/`str_regex_replace()`. Combine with bitwise OR. Both SDKs expose them as `StringRegexFlags`.

| Flag | Effect |
| --- | --- |
| `DEFAULT` | No flags |
| `CASE_INSENSITIVE` | Case-insensitive matching |
| `MULTILINE` | `^` and `$` match the start/end of any line, not just the start/end of the input |
| `DOTALL` | `.` matches any character, including line terminators |
| `UNIX_LINES` | Treat only `\n` as a line terminator |
| `GLOBAL` | Replace every match, not just the first. Only applicable to `regexReplace`/`str_regex_replace` |

Patterns use International Components for Unicode (ICU) regex syntax.

::: validate regex patterns before production use
`regexCompare()`/`str_regex_compare()` and `regexReplace()`/`str_regex_replace()` evaluate patterns on the server. A pathological pattern (for example, nested quantifiers like `(a+)+`) can cause catastrophic backtracking and consume a server thread. Running a regex modify operation across a whole set multiplies this cost per matching record. Test regex patterns and their performance impact on a non-production cluster before using them in a set-wide query.
:::

## Type conversion

| Java | Python | Effect |
| --- | --- | --- |
| `readAsString()` | `read_as_string()` | Convert the bin’s value to its string representation. Accepts int, float, string, bool, or valid UTF-8 blob bins. This is type-agnostic: it operates on any convertible bin type, not just strings, so it’s not named `toString()`/`to_string()`, avoiding a naming collision with generic conversions |
| `stringToInteger()` | `str_to_integer()` | Parse the string bin as an int64 |
| `stringToDouble()` | `str_to_double()` | Parse the string bin as a float64 |
| `stringToBlob()` | `str_to_blob()` | Reinterpret the string bin’s UTF-8 bytes as a blob |

In Java, `readAsString()` is available on the write builders (`session.update(...)`/`session.upsert(...)`), not on `session.query(key).bin(...)`. Python offers `read_as_string()` on both.

A parse failure (for example, `stringToInteger()` / `str_to_integer()` on a non-numeric string) fails the call with `OP_NOT_APPLICABLE`, raised as `BinOpInvalidException` (Java) / `BinOpInvalidError` (Python). Guard with `isNumeric(StringNumericType.INT)` / `str_is_numeric(StringNumericType.INT)` first when the input isn’t trusted, or catch the exception. Calling `readAsString()`/`read_as_string()` on a bin type outside the supported list (for example, a List or Map bin) fails with `BIN_TYPE_ERROR`.

-   [Java](#tab-panel-6126)
-   [Python](#tab-panel-6127)

```java
DataSet docs = DataSet.of("test", "docs");

String key = "row1";

session.upsert(docs.id(key)).bin("count").setTo("42").execute();

RecordStream stream = session.query(docs.id(key))

    .bin("count").stringToInteger()

    .execute();

Record rec = stream.getFirst().orElseThrow().recordOrThrow();

System.out.println("parsed: " + rec.operationResult(0).getLong());

stream.close();
```

> 📖 **API reference**: [`DataSet.of(...)`](https://javadoc.io/doc/com.aerospike/aerospike-client-sdk/latest/com/aerospike/client/sdk/DataSet.html#of%28java.lang.String%2Cjava.lang.String%29) | [`DataSet.id(...)`](https://javadoc.io/doc/com.aerospike/aerospike-client-sdk/latest/com/aerospike/client/sdk/DataSet.html#id%28java.lang.String%29) | [`Session.query(Key)`](https://javadoc.io/doc/com.aerospike/aerospike-client-sdk/latest/com/aerospike/client/sdk/Session.html#query%28com.aerospike.client.sdk.Key%29) | [`ChainableQueryBuilder.execute()`](https://javadoc.io/doc/com.aerospike/aerospike-client-sdk/latest/com/aerospike/client/sdk/ChainableQueryBuilder.html#execute%28%29) | [`RecordStream.getFirst()`](https://javadoc.io/doc/com.aerospike/aerospike-client-sdk/latest/com/aerospike/client/sdk/RecordStream.html#getFirst%28%29) | [`RecordStream.close()`](https://javadoc.io/doc/com.aerospike/aerospike-client-sdk/latest/com/aerospike/client/sdk/RecordStream.html#close%28%29) | [`RecordResult.recordOrThrow()`](https://javadoc.io/doc/com.aerospike/aerospike-client-sdk/latest/com/aerospike/client/sdk/RecordResult.html#recordOrThrow%28%29) | [`Record.operationResult(...)`](https://javadoc.io/doc/com.aerospike/aerospike-client-sdk/latest/com/aerospike/client/sdk/Record.html#operationResult%28int%29)

```python
docs = DataSet.of("test", "docs")

key = docs.id("row1")

await session.upsert(key).put({"count": "42"}).execute()

stream = await session.query(key).bin("count").str_to_integer().execute()

row = await stream.first_or_raise()

print(f"parsed: {row.operation_result(0)}")

stream.close()
```

> 📖 **API reference**: [`DataSet.of()`](https://aerospike-python-sdk.readthedocs.io/en/latest/api/dataset.html#aerospike%5Fsdk.dataset.DataSet.of) | [`DataSet.id()`](https://aerospike-python-sdk.readthedocs.io/en/latest/api/dataset.html#aerospike%5Fsdk.dataset.DataSet.id) | [`Session.query()`](https://aerospike-python-sdk.readthedocs.io/en/latest/api/session.html#aerospike%5Fsdk.aio.session.Session.query) | [`Session.upsert()`](https://aerospike-python-sdk.readthedocs.io/en/latest/api/session.html#aerospike%5Fsdk.aio.session.Session.upsert) | [`WriteSegmentBuilder.put()`](https://aerospike-python-sdk.readthedocs.io/en/latest/api/write-segment.html#aerospike%5Fsdk.aio.operations.query.WriteSegmentBuilder.put) | [`RecordResult.operation_result()`](https://aerospike-python-sdk.readthedocs.io/en/latest/api/record-result.html#aerospike%5Fsdk.record%5Fresult.RecordResult.operation%5Fresult) | [`RecordStream.first_or_raise()`](https://aerospike-python-sdk.readthedocs.io/en/latest/api/record-stream.html#aerospike%5Fsdk.record%5Fstream.RecordStream.first%5For%5Fraise) | [`RecordStream.close()`](https://aerospike-python-sdk.readthedocs.io/en/latest/api/record-stream.html#aerospike%5Fsdk.record%5Fstream.RecordStream.close)

## String expressions

`StringExp` (Java) and `Exp.string_*` (Python) mirror the same operations as expressions, for use in `.where(...)` filters and `selectFrom`/`select_from` projections. Modify-style expressions such as `upper` and `replace` return the transformed value without writing it back to the bin. Pass the result to a write operation if you need to persist it. Modify-style expressions take the write flags as their first argument (`0` for the default), followed by the operation’s arguments and the source expression last.

::: java: ops projection is key-reads only
As covered in [Project reads with ops projection](https://aerospike.com/docs/develop/client/sdk/usage/read/#project-reads-with-ops-projection), Java dataset scans (`session.query(dataSet).where(...)`) reject bin-level `.selectFrom(...)`. The following example combines `.where(...)` with `.selectFrom(...)` on a key read instead. Python’s chained `.select_from(...)` works on both key reads and filtered set queries.
:::

-   [Java](#tab-panel-6128)
-   [Python](#tab-panel-6129)

```java
DataSet docs = DataSet.of("test", "docs");

String key = "row1";

session.upsert(docs.id(key)).bin("email").setTo("Alice@Example.com").execute();

// Filter plus a projection that lowercases the email, both on a key read.

RecordStream stream = session.query(docs.id(key))

    .where(StringExp.startsWith(

        Exp.val("alice"),

        StringExp.lower(0, Exp.stringBin("email"))))

    .bin("emailLower").selectFrom(StringExp.lower(0, Exp.stringBin("email")))

    .execute();

Record rec = stream.getFirst().orElseThrow().recordOrThrow();

System.out.println("lowercased email: " + rec.getString("emailLower"));

stream.close();
```

> 📖 **API reference**: [`StringExp.startsWith(...)`](https://javadoc.io/doc/com.aerospike/aerospike-client-sdk/latest/com/aerospike/client/sdk/exp/StringExp.html#startsWith%28com.aerospike.client.sdk.exp.Exp%2Ccom.aerospike.client.sdk.exp.Exp%29) | [`StringExp.lower(...)`](https://javadoc.io/doc/com.aerospike/aerospike-client-sdk/latest/com/aerospike/client/sdk/exp/StringExp.html#lower%28int%2Ccom.aerospike.client.sdk.exp.Exp%29) | [`Exp.stringBin(...)`](https://javadoc.io/doc/com.aerospike/aerospike-client-sdk/latest/com/aerospike/client/sdk/exp/Exp.html#stringBin%28java.lang.String%29) | [`Exp.val(...)`](https://javadoc.io/doc/com.aerospike/aerospike-client-sdk/latest/com/aerospike/client/sdk/exp/Exp.html#val%28java.lang.String%29) | [`ChainableQueryBuilder.where(...)`](https://javadoc.io/doc/com.aerospike/aerospike-client-sdk/latest/com/aerospike/client/sdk/ChainableQueryBuilder.html#where%28com.aerospike.client.sdk.exp.Exp%29) | [`QueryBinBuilder.selectFrom(...)`](https://javadoc.io/doc/com.aerospike/aerospike-client-sdk/latest/com/aerospike/client/sdk/QueryBinBuilder.html#selectFrom%28com.aerospike.client.sdk.exp.Exp%29)

```python
docs = DataSet.of("test", "docs")

key = docs.id("row1")

await session.upsert(key).put({"email": "Alice@Example.com"}).execute()

# Filter: only match records where the lowercased email starts with "alice".

# A full-dataset scan works here for Python. Java's equivalent must be a

# key read: see the "Java: ops projection is key-reads only" note.

stream = await (

    session.query(docs)

    .where(Exp.string_starts_with(

        Exp.val("alice"),

        Exp.string_lower(0, Exp.string_bin("email"))))

    .bin("email_lower").select_from(Exp.string_lower(0, Exp.string_bin("email")))

    .execute()

)

row = await stream.first_or_raise()

record = row.record_or_raise()

print(f"lowercased email: {record.bins['email_lower']}")

stream.close()
```

> 📖 **API reference**: [`Exp`](https://aerospike-python-sdk.readthedocs.io/en/latest/api/exp.html) | [`QueryBuilder.where()`](https://aerospike-python-sdk.readthedocs.io/en/latest/api/query.html#aerospike%5Fsdk.aio.operations.query.QueryBuilder.where) | [`QueryBinBuilder.select_from()`](https://aerospike-python-sdk.readthedocs.io/en/latest/api/query.html#aerospike%5Fsdk.aio.operations.query.QueryBinBuilder.select%5Ffrom) | [`RecordResult.record_or_raise()`](https://aerospike-python-sdk.readthedocs.io/en/latest/api/record-result.html#aerospike%5Fsdk.record%5Fresult.RecordResult.record%5For%5Fraise)

::: use stringexp/exp.string_* for string logic in ael-based queries
The Aerospike Expression Language (AEL) text syntax (`session.query(users).where("$.name.length() > 5")`, see [Overview](https://aerospike.com/docs/develop/client/sdk/concepts/ael)) doesn’t document string function coverage on this site yet. Use `StringExp`/`Exp.string_*`, shown in this section, for string logic inside `.where(...)` and projections.
:::

## Nested strings

Both APIs can target a string value nested inside a list or map through the same CDT path methods used for collection operations (`onMapKey()` / `on_map_key()`, `onListIndex()` / `on_list_index()`, and similar). The nested value must already be a string. Operations on a non-string leaf return a bin-type error. `CREATE_ONLY` can’t be combined with a nested path (`PARAMETER_ERROR`).

-   [Java](#tab-panel-6130)
-   [Python](#tab-panel-6131)

```java
// Additional import for this example:

import java.util.Map;

DataSet docs = DataSet.of("test", "docs");

String key = "row1";

session.upsert(docs.id(key))

    .bin("profile").setTo(Map.of("bio", "  Loves hiking  "))

    .execute();

// Trim the nested "bio" string inside the "profile" map, then read the map back.

RecordStream stream = session.update(docs.id(key))

    .bin("profile").onMapKey("bio").trim()

    .bin("profile").get()

    .execute();

Record rec = stream.getFirst().orElseThrow().recordOrThrow();

System.out.println("profile after trim: " + rec.operationResult(1).getMap());

stream.close();
```

> 📖 **API reference**: [`BinBuilder.onMapKey(...)`](https://javadoc.io/doc/com.aerospike/aerospike-client-sdk/latest/com/aerospike/client/sdk/BinBuilder.html#onMapKey%28java.lang.String%29) | [`Session.update(DataSet)`](https://javadoc.io/doc/com.aerospike/aerospike-client-sdk/latest/com/aerospike/client/sdk/Session.html#update%28com.aerospike.client.sdk.DataSet%29) | [`Record.operationResult(...)`](https://javadoc.io/doc/com.aerospike/aerospike-client-sdk/latest/com/aerospike/client/sdk/Record.html#operationResult%28int%29)

```python
docs = DataSet.of("test", "docs")

key = docs.id("row1")

await session.upsert(key).put({"profile": {"bio": "  Loves hiking  "}}).execute()

# Trim the nested "bio" string inside the "profile" map, then read the map back.

stream = await (

    session.update(key)

    .bin("profile").on_map_key("bio").str_trim()

    .bin("profile").get()

    .execute()

)

row = await stream.first_or_raise()

print(f"profile after trim: {row.operation_result(1)}")

stream.close()
```

> 📖 **API reference**: [`WriteBinBuilder.on_map_key()`](https://aerospike-python-sdk.readthedocs.io/en/latest/api/query.html#aerospike%5Fsdk.aio.operations.query.WriteBinBuilder.on%5Fmap%5Fkey) | [`Session.update()`](https://aerospike-python-sdk.readthedocs.io/en/latest/api/session.html#aerospike%5Fsdk.aio.session.Session.update) | [`RecordResult.operation_result()`](https://aerospike-python-sdk.readthedocs.io/en/latest/api/record-result.html#aerospike%5Fsdk.record%5Fresult.RecordResult.operation%5Fresult)

## Version requirements

String read/modify operations require **Aerospike Database 8.2.0 or later**. Sending these operations to an older server returns an error. During a rolling upgrade, don’t issue these operations until every node in the cluster reports Database 8.2.0 or later — a partially upgraded cluster accepts them on upgraded nodes and rejects them on nodes that aren’t upgraded yet. Check your SDK release notes for the minimum client version. See [Version Compatibility](https://aerospike.com/docs/develop/client/sdk/reference/compatibility).

## Complete example

This example is self-contained. It lists every import needed to run standalone.

-   [Java](#tab-panel-6132)
-   [Python](#tab-panel-6133)

```java
import com.aerospike.client.sdk.Cluster;

import com.aerospike.client.sdk.ClusterDefinition;

import com.aerospike.client.sdk.DataSet;

import com.aerospike.client.sdk.Record;

import com.aerospike.client.sdk.RecordStream;

import com.aerospike.client.sdk.Session;

import com.aerospike.client.sdk.policy.Behavior;

public class StringOperationsExample {

    public static void main(String[] args) {

        try (Cluster cluster = new ClusterDefinition("localhost", 3000).connect()) {

            Session session = cluster.createSession(Behavior.DEFAULT);

            DataSet docs = DataSet.of("test", "docs");

            String key = "string-example-doc";

            // Seed data so the example is repeatable.

            session.upsert(docs.id(key)).bin("title").setTo("  the Quick Brown Fox  ").execute();

            // Read operations: inspect the string without modifying it.

            RecordStream readStream = session.query(docs.id(key))

                .bin("title").strlen()

                .bin("title").contains("Fox")

                .execute();

            Record read = readStream.getFirst().orElseThrow().recordOrThrow();

            System.out.println("length: " + read.operationResult(0).getLong());

            System.out.println("contains 'Fox': " + read.operationResult(1).getBoolean());

            readStream.close();

            // Modify operations: trim, uppercase, and pad the bin in place,

            // then read the result back in the same call.

            RecordStream modifyStream = session.update(docs.id(key))

                .bin("title").trim()

                .bin("title").upper()

                .bin("title").padEnd(30, ".")

                .bin("title").get()

                .execute();

            Record modified = modifyStream.getFirst().orElseThrow().recordOrThrow();

            System.out.println("after modify: " + modified.operationResult(3).getString());

            modifyStream.close();

            // Type conversion: parse a numeric-looking string bin.

            session.upsert(docs.id(key)).bin("count").setTo("42").execute();

            RecordStream convertStream = session.query(docs.id(key))

                .bin("count").stringToInteger()

                .execute();

            Record converted = convertStream.getFirst().orElseThrow().recordOrThrow();

            System.out.println("parsed count: " + converted.operationResult(0).getLong());

            convertStream.close();

        }

    }

}
```

> 📖 **API reference**: [`ClusterDefinition(String,int)`](https://javadoc.io/doc/com.aerospike/aerospike-client-sdk/latest/com/aerospike/client/sdk/ClusterDefinition.html#%3Cinit%3E%28java.lang.String%2Cint%29) | [`ClusterDefinition.connect()`](https://javadoc.io/doc/com.aerospike/aerospike-client-sdk/latest/com/aerospike/client/sdk/ClusterDefinition.html#connect%28%29) | [`Cluster.createSession(Behavior)`](https://javadoc.io/doc/com.aerospike/aerospike-client-sdk/latest/com/aerospike/client/sdk/Cluster.html#createSession%28com.aerospike.client.sdk.policy.Behavior%29) | [`DataSet.of(...)`](https://javadoc.io/doc/com.aerospike/aerospike-client-sdk/latest/com/aerospike/client/sdk/DataSet.html#of%28java.lang.String%2Cjava.lang.String%29) | [`Session.upsert(DataSet)`](https://javadoc.io/doc/com.aerospike/aerospike-client-sdk/latest/com/aerospike/client/sdk/Session.html#upsert%28com.aerospike.client.sdk.DataSet%29) | [`Session.query(Key)`](https://javadoc.io/doc/com.aerospike/aerospike-client-sdk/latest/com/aerospike/client/sdk/Session.html#query%28com.aerospike.client.sdk.Key%29) | [`Session.update(DataSet)`](https://javadoc.io/doc/com.aerospike/aerospike-client-sdk/latest/com/aerospike/client/sdk/Session.html#update%28com.aerospike.client.sdk.DataSet%29) | [`RecordStream.getFirst()`](https://javadoc.io/doc/com.aerospike/aerospike-client-sdk/latest/com/aerospike/client/sdk/RecordStream.html#getFirst%28%29) | [`RecordStream.close()`](https://javadoc.io/doc/com.aerospike/aerospike-client-sdk/latest/com/aerospike/client/sdk/RecordStream.html#close%28%29) | [`Record.operationResult(...)`](https://javadoc.io/doc/com.aerospike/aerospike-client-sdk/latest/com/aerospike/client/sdk/Record.html#operationResult%28int%29)

```python
import asyncio

from aerospike_sdk import Behavior, ClusterDefinition, DataSet

async def main():

    async with await ClusterDefinition("localhost", 3000).connect() as cluster:

        session = cluster.create_session(Behavior.DEFAULT)

        docs = DataSet.of("test", "docs")

        key = docs.id("string-example-doc")

        # Seed data so the example is repeatable.

        await session.upsert(key).put({"title": "  the Quick Brown Fox  "}).execute()

        # Read operations: inspect the string without modifying it.

        stream = await (

            session.query(key)

            .bin("title").str_strlen()

            .bin("title").str_contains("Fox")

            .execute()

        )

        read = await stream.first_or_raise()

        print(f"length: {read.operation_result(0)}")

        print(f"contains 'Fox': {read.operation_result(1)}")

        stream.close()

        # Modify operations: trim, uppercase, and pad the bin in place,

        # then read the result back in the same call.

        stream = await (

            session.update(key)

            .bin("title").str_trim()

            .bin("title").str_upper()

            .bin("title").str_pad_end(30, ".")

            .bin("title").get()

            .execute()

        )

        modified = await stream.first_or_raise()

        print(f"after modify: {modified.operation_result(3)}")

        stream.close()

        # Type conversion: parse a numeric-looking string bin.

        await session.upsert(key).put({"count": "42"}).execute()

        stream = await session.query(key).bin("count").str_to_integer().execute()

        converted = await stream.first_or_raise()

        print(f"parsed count: {converted.operation_result(0)}")

        stream.close()

if __name__ == "__main__":

    asyncio.run(main())
```

> 📖 **API reference**: [`ClusterDefinition`](https://aerospike-python-sdk.readthedocs.io/en/latest/api/cluster-definition.html) | [`ClusterDefinition.connect()`](https://aerospike-python-sdk.readthedocs.io/en/latest/api/cluster-definition.html) | [`Cluster.create_session()`](https://aerospike-python-sdk.readthedocs.io/en/latest/api/cluster.html) | [`DataSet.of()`](https://aerospike-python-sdk.readthedocs.io/en/latest/api/dataset.html#aerospike%5Fsdk.dataset.DataSet.of) | [`DataSet.id()`](https://aerospike-python-sdk.readthedocs.io/en/latest/api/dataset.html#aerospike%5Fsdk.dataset.DataSet.id) | [`Session.query()`](https://aerospike-python-sdk.readthedocs.io/en/latest/api/session.html#aerospike%5Fsdk.aio.session.Session.query) | [`Session.update()`](https://aerospike-python-sdk.readthedocs.io/en/latest/api/session.html#aerospike%5Fsdk.aio.session.Session.update) | [`Session.upsert()`](https://aerospike-python-sdk.readthedocs.io/en/latest/api/session.html#aerospike%5Fsdk.aio.session.Session.upsert) | [`RecordResult.operation_result()`](https://aerospike-python-sdk.readthedocs.io/en/latest/api/record-result.html#aerospike%5Fsdk.record%5Fresult.RecordResult.operation%5Fresult) | [`RecordStream.first_or_raise()`](https://aerospike-python-sdk.readthedocs.io/en/latest/api/record-stream.html#aerospike%5Fsdk.record%5Fstream.RecordStream.first%5For%5Fraise) | [`RecordStream.close()`](https://aerospike-python-sdk.readthedocs.io/en/latest/api/record-stream.html#aerospike%5Fsdk.record%5Fstream.RecordStream.close)

## API reference summary

| Category | Java | Python |
| --- | --- | --- |
| Read | `strlen()`, `substr(...)`, `charAt(...)`, `find(...)`, `contains(...)`, `startsWith(...)`, `endsWith(...)`, `byteLength()`, `isNumeric(...)`, `isUpper()`, `isLower()`, `split(...)`, `b64Decode()`, `regexCompare(...)` | `str_strlen()`, `str_substr(...)`, `str_char_at(...)`, `str_find(...)`, `str_contains(...)`, `str_starts_with(...)`, `str_ends_with(...)`, `str_byte_length()`, `str_is_numeric(...)`, `str_is_upper()`, `str_is_lower()`, `str_split(...)`, `str_b64_decode()`, `str_regex_compare(...)` |
| Modify | `insert(...)`, `overwrite(...)`, `concat(...)`, `append(...)`, `prepend(...)`, `snip(...)`, `replace(...)`, `replaceAll(...)`, `upper(...)`, `lower(...)`, `caseFold(...)`, `normalizeNfc(...)`, `trimStart(...)`, `trimEnd(...)`, `trim(...)`, `padStart(...)`, `padEnd(...)`, `repeat(...)`, `regexReplace(...)` | `str_insert(...)`, `str_overwrite(...)`, `str_concat(...)`, `str_append(...)`, `str_prepend(...)`, `str_snip(...)`, `str_replace(...)`, `str_replace_all(...)`, `str_upper(...)`, `str_lower(...)`, `str_case_fold(...)`, `str_normalize_nfc(...)`, `str_trim_start(...)`, `str_trim_end(...)`, `str_trim(...)`, `str_pad_start(...)`, `str_pad_end(...)`, `str_repeat(...)`, `str_regex_replace(...)` |
| Type conversion | `readAsString()`, `stringToInteger()`, `stringToDouble()`, `stringToBlob()` | `read_as_string()`, `str_to_integer()`, `str_to_double()`, `str_to_blob()` |
| Expressions | `StringExp.*` | `Exp.string_*` |
| Write flags | `StringWriteOptions` (fluent) / `StringWriteFlags.DEFAULT`, `CREATE_ONLY`, `UPDATE_ONLY`, `NO_FAIL` | `StringWriteFlags.DEFAULT`, `CREATE_ONLY`, `UPDATE_ONLY`, `NO_FAIL` (see [String write flags](#string-write-flags)) |
| Regex flags | `StringRegexFlags.*` | `StringRegexFlags.*` |
| Positional results | `Record.operationResult(i)` | `RecordResult.operation_result(i)` |

## Next steps

Update Records

Modify bins, increment numbers, and update CDTs.

[Update Records →](https://aerospike.com/docs/develop/client/sdk/usage/update)

AEL query language

Filter records with readable expression syntax.

[AEL →](https://aerospike.com/docs/develop/client/sdk/concepts/ael)

Data Model

Understand namespaces, sets, and bins.

[Data Model →](https://aerospike.com/docs/develop/client/sdk/concepts/data-model)

Error Handling

Handle type-mismatch and parse errors.

[Error Handling →](https://aerospike.com/docs/develop/client/sdk/concepts/errors)