IBM RPG and Java: Data Type Comparison – Precise Mappings, Real Migration Risks and Best Practices for IBM i
A practical guide for modernization teams, RPG developers, Java developers, architects, testers, and curious IBM i practitioners.

Practical guide for IBM i modernization and RPG/Java integration
Both RPG and Java understand numbers, text, dates, and booleans.
The dangerous errors almost never come from syntax. They appear where a technically valid mapping is semantically wrong.
The focus is not on an academic catalog of types, but on the decisions that actually matter in real projects: precision, value ranges, CCSID handling, time semantics, leading zeros, and proper domain modeling.
1. Summary: The Critical Difference
RPG data types are deeply tied to classical business data processing, Db2 for i, externally described files, DDS, and fixed field definitions.
Java data types live in an object-oriented, JVM-based world. There we distinguish between primitive types such as int, long, or double and object types such as String, BigDecimal, LocalDate, Instant, or custom records.
Business intent determines the Java type — not visual similarity to the RPG field.
A
packed(9:2) can represent a price, a weight, a discount, or an account balance.A customer number with leading zeros is usually a business key, not a number.
A timestamp might be local posting time or a technical event instant.
These distinctions must be visible in your mapping layer.
The highest-risk mistakes in real projects fall into four classic categories: mapping Packed Decimal to double, adopting timestamps without timezone awareness, discovering CCSID conversion issues only in production, and stripping leading zeros from alphanumeric IDs.
Every one of these errors is technically trivial to explain — and extremely expensive in practice.
2. Why Data Types Matter So Much on IBM i
On IBM i, data types rarely live only in local variables. They are part of physical files, SQL tables, DDS descriptions, externally described data structures, RPG programs, CL flows, SQL procedures, and interfaces.
A field is never just “a number” — it might be inventory quantity, a customer key, a monetary amount, or a historic alphanumeric field whose leading zeros have been part of business processes for years.
When you put Java services in front of existing IBM i data, you must separate two perspectives: what is technically possible and what is semantically correct.
Technically you can read a numeric key as a long. Semantically that can still be dangerous if the same key appears in documents, EDI messages, or external APIs with leading zeros preserved.
“It compiles” is not a quality criterion for type migration. What matters is whether values retain the same meaning after import, calculation, JSON serialization, database update, and return to RPG.
3. RPG and Java Side-by-Side
The following comparison table is deliberately practical. It does not show absolute memory truth for Java objects. Instead it answers the more important question:
Which Java type best preserves the business meaning of the RPG field?
| RPG / IBM i Concept | Technically Possible in Java | Usually Recommended | Migration Considerations |
|---|---|---|---|
PACKED e.g. packed(9:2) |
double, BigDecimal, String |
BigDecimal |
Never default to double for monetary values. Explicitly test scale and rounding mode. |
ZONED |
int, long, BigDecimal, String |
Depends on purpose | Check whether calculations are performed or whether leading zeros, display format, or code character matter. |
INT(5) |
short, int |
Usually int |
short rarely saves meaningful space and often makes code clumsier. |
INT(10) |
int |
int |
Test boundary values and SQL mapping. |
INT(20) |
long |
long |
Good for large counters and technical sequences; review IDs from a business perspective. |
UNS |
long + unsigned helpers or BigInteger |
Context-dependent | Java has no direct unsigned primitive equivalent. Be especially careful with 64-bit values. |
FLOAT(4), FLOAT(8) |
float, double |
double for approximations only |
Suitable for measurements, simulations, technical approximations. Risky for cent amounts. |
DATE |
String, java.sql.Date, LocalDate |
LocalDate |
Date without time and without timezone. Do not confuse format with business meaning. |
TIME |
String, LocalTime |
LocalTime |
Time of day without date. Do not misuse as a global event timestamp. |
TIMESTAMP |
LocalDateTime, Instant, OffsetDateTime, String |
LocalDateTime for local business time, Instant for technical events |
Document your timezone decision. Otherwise you will get DST and location bugs. |
CHAR, VARCHAR |
String |
String + clear rules |
Handle trimming, padding, leading spaces, CCSID, and Unicode deliberately. |
GRAPHIC, UCS-2 |
String |
String |
Java works with UTF-16 code units. Character length ≠ byte length. |
IND / Indicator |
boolean |
boolean or business status |
A single flag is fine. Multiple dependent flags usually deserve an enum. |
| Binary data, BLOB, Byte fields | byte[], ByteBuffer, InputStream |
Context-dependent | Never accidentally interpret as text. Encoding is not a magic repair tool. |
| Pointer | References, handles, wrapper objects | New API or object model | Do not recreate pointer logic. Prefer clean parameter objects and explicit interfaces. |
4. Numbers: Integer, Packed, Zoned, Float
With numeric fields the first question must always be: what kind of number are we dealing with — an exact decimal value, a technical integer, a business key, or an approximation?
This distinction matters more than raw field length.
Integer: Technical Whole Numbers
RPG offers integer variants of different sizes. Java provides the primitive types byte, short, int, and long.
In modern Java business logic, int is commonly used for ordinary counters and long for large values.
| RPG | Java | Typical Use | Test Idea |
|---|---|---|---|
int(5) |
short or int |
Small technical values | Check minimum, maximum, and negative values. |
int(10) |
int |
Standard integer for counters, status values, quantities without decimals | Test SQL insert/update with boundary values. |
int(20) |
long |
Large counters, sequences, technical IDs | Verify JSON serialization and JavaScript consumers for very large values. |
uns(...) |
long + unsigned helpers or BigInteger |
Positive values without sign | Explicitly test values above the signed range. |
Float and Double: Technically Possible, Often Semantically Risky
Floating-point numbers are fast and appropriate for measurements, simulations, or technical approximations. For monetary and commercial values they are usually the wrong choice.
double cannot represent many decimal values exactly. For monetary values BigDecimal is the safer, more robust choice.double sum = 0.1 + 0.2;
System.out.println(sum); // usually not exactly 0.3
BigDecimal price = new BigDecimal("0.10");
BigDecimal tax = new BigDecimal("0.20");
System.out.println(price.add(tax)); // exactly 0.30
5. Packed Decimal and BigDecimal
Packed Decimal is a very common choice in RPG for monetary and commercial values. Java has no primitive equivalent.
The primary counterpart is java.math.BigDecimal.
For monetary amounts, prices, discounts, taxes, and exact quantities with decimal places: use
BigDecimal.For technical approximations:
double.For pure counters:
int or long.For IDs that carry leading zeros: usually
String or a dedicated Value Object.**free
dcl-s price packed(9:2) inz(1299.95);
dcl-s quantity packed(5:0) inz(3);
dcl-s total packed(11:2);
total = price * quantity;
import java.math.BigDecimal;
import java.math.RoundingMode;
BigDecimal price = new BigDecimal("1299.95");
BigDecimal quantity = new BigDecimal("3");
BigDecimal total = price.multiply(quantity)
.setScale(2, RoundingMode.HALF_UP);
The tests below are intentionally small. They are meant to validate typical migration assumptions around scale, leading zeros, time semantics, and Unicode behavior — not to cover an entire domain model.
import static org.junit.jupiter.api.Assertions.assertEquals;
import java.math.BigDecimal;
import org.junit.jupiter.api.Test;
class DecimalMappingTest {
@Test
void keepsScaleForPackedAmount() {
BigDecimal amount = new BigDecimal("9999999.99").setScale(2);
assertEquals(new BigDecimal("9999999.99"), amount);
assertEquals(2, amount.scale());
}
}
new BigDecimal(0.1) already inherits the imprecision of the double literal.For constant decimal values prefer string literals or
BigDecimal.valueOf(...) — and always verify scale explicitly for business amounts.6. Date, Time, and Timestamps
RPG has dedicated data types for date, time, and timestamp. Since Java 8 the java.time package provides a modern, rich API.
The key point: LocalDate, LocalTime, LocalDateTime, Instant, and OffsetDateTime answer different questions.
| Business Question | RPG | Java | Guidance |
|---|---|---|---|
| Which calendar day? | date |
LocalDate |
Ideal for birthdays, delivery dates, posting dates. |
| What time of day? | time |
LocalTime |
Good for opening hours or shift start times. |
| Which local business time? | timestamp |
LocalDateTime |
Use only when timezone is irrelevant or handled separately. |
| Which technical instant? | timestamp + context |
Instant |
Perfect for logs, events, audit trails, and technical synchronization. |
| Which time with offset in API contract? | Depends on design | OffsetDateTime |
Useful for REST APIs when the offset itself carries meaning. |
An RPG
timestamp is mapped to Java LocalDateTime and later interpreted as a global instant.This works until the first location change, daylight-saving transition, or external consumer in another timezone.
import java.time.Instant;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.OffsetDateTime;
LocalDate invoiceDate = LocalDate.of(2026, 6, 9);
LocalDateTime localBookingTime = LocalDateTime.of(2026, 6, 9, 14, 30);
Instant technicalEventTime = Instant.now();
OffsetDateTime apiTimestamp = OffsetDateTime.now();
import static org.junit.jupiter.api.Assertions.assertEquals;
import java.time.Instant;
import java.time.OffsetDateTime;
import org.junit.jupiter.api.Test;
class TimestampMappingTest {
@Test
void apiTimestampKeepsOffsetInformation() {
OffsetDateTime value = OffsetDateTime.parse("2026-06-09T14:30:00+02:00");
Instant instant = value.toInstant();
assertEquals("2026-06-09T12:30:00Z", instant.toString());
}
}
7. Text, String, Unicode, and CCSID
Text is often more dangerous than numbers in integration projects. RPG and Db2 for i fields can use different CCSIDs, fixed or variable length, padding, and historical EBCDIC contexts.
Java treats String as a Unicode-oriented object type.
When umlauts, special characters, or external JSON/XML interfaces are involved, you must test the entire path: Db2 for i → JDBC/JTOpen → Job CCSID → API encoding → and back to RPG.
A test with
ABC123 proves almost nothing here.| RPG / IBM i | Java | Technically Possible | Business Question to Clarify |
|---|---|---|---|
char(n) |
String |
Direct text mapping | Fixed length, padding, trimming, leading spaces. |
varchar(n) |
String |
Variable length | Parameter passing, maximum length, validation. |
| Numeric-looking ID | String or Value Object |
long would often work |
Leading zeros, check digits, external representation. |
graphic |
String |
Text mapping | Double-byte characters and conversion behavior. |
ucs2 |
String |
Unicode-near storage | Do not blindly equate with modern Unicode code-point logic. |
String text = "tiny-tool.de 🚀";
int utf16Units = text.length();
int codePoints = text.codePointCount(0, text.length());
System.out.println("UTF-16 code units: " + utf16Units);
System.out.println("Unicode code points: " + codePoints);
import static org.junit.jupiter.api.Assertions.assertEquals;
import org.junit.jupiter.api.Test;
class CustomerNumberTest {
@Test
void keepsLeadingZeros() {
CustomerNumber number = new CustomerNumber("000123");
assertEquals("000123", number.value());
}
}
8. Binary Data, Pointers, and References
Binary data should be treated as binary data in Java: byte[], ByteBuffer, InputStream, or appropriate streaming types depending on the framework.
The classic mistake is accidentally sending binary data through a text conversion.
Pointers are a different story. RPG can work with pointers; Java abstracts memory access through references and object models.
A direct mechanical translation is rarely a good goal.
Pointer logic is a strong warning sign. It almost always pays off to make a small architectural cut: introduce clean DTOs, explicit parameter objects, and a clear interface instead of trying to recreate “pointer thinking” inside Java.
9. Common Migration Pitfalls
The following mistakes appear especially often in RPG-to-Java projects. They typically occur when teams try to “just quickly pass a field through.”
| Mistake | Technically Possible? | Why Semantically Risky? | Better Approach |
|---|---|---|---|
Mapping packed blindly to double |
Yes | Rounding and comparison errors with decimal values. | Use BigDecimal and explicitly test scale + rounding mode. |
| Treating leading-zero IDs as numbers | Yes | 000123 becomes 123; documents, APIs or EDI can break. |
Use String or a CustomerNumber Value Object. |
Using LocalDateTime for technical events |
Yes | No unique instant without zone or offset. | Use Instant or OffsetDateTime. |
| Ignoring CCSID | Unfortunately yes | Umlauts, special characters and external interfaces break late in the project. | End-to-end encoding tests with real production data. |
| Taking RPG field length directly as Java String limit | Partially | Byte length, UTF-16 code units, code points, and business length are different concepts. | Document technical length and business validation separately. |
| Blindly adopting anonymous indicators | Yes | flag1, flag2, flag3 do not explain any business rule. |
Use speaking booleans or an enum for status modeling. |
Automatically mapping Zoned Decimal to int |
Yes | Decimal places, leading zeros or display logic get lost. | First clarify: number, amount, code, or text? |
10. Value Objects and Domain Modeling
Java modernization is not only about type mapping. It is also an opportunity to make business rules visible in code.
An amount is not just any BigDecimal. A customer number is not just a String.
Small Value Objects prevent the same validation logic from appearing in ten places — or missing from nine of them.
import java.math.BigDecimal;
import java.math.RoundingMode;
public record Money(BigDecimal amount) {
public Money {
if (amount == null) {
throw new IllegalArgumentException("amount must not be null");
}
amount = amount.setScale(2, RoundingMode.HALF_UP);
}
public Money add(Money other) {
return new Money(this.amount.add(other.amount));
}
}
public record CustomerNumber(String value) {
public CustomerNumber {
if (value == null || !value.matches("\\d{6}")) {
throw new IllegalArgumentException("customer number must have exactly 6 digits");
}
}
}
public enum OrderStatus {
OPEN,
RELEASED,
SHIPPED,
CANCELLED
}
Not every field needs its own object. But values that carry business rules, external representation requirements, or high error risk are excellent candidates: money, customer numbers, item numbers, quantities, currencies, status values, and technical event timestamps.
11. Best Practices and Test Ideas
- Clarify business meaning first: Price, quantity, key, status, date, and text are different things — even when they look technically similar.
- Handle decimal values consistently: Prefer
BigDecimalfor money, prices, and commercial values in Java. - Document scale explicitly: In
packed(9:2)the:2is business-critical, not decoration. - Model date and time cleanly:
LocalDatefor dates,Instantfor technical instants,OffsetDateTimefor API timestamps that include offset. - Test CCSID and Unicode thoroughly: Not only with
ABC123, but withÄÖÜ,ß, accents, special characters, and real production examples. - Do not blindly numericize IDs: When leading zeros, check digits, or external display matter,
Stringor a Value Object is usually the correct choice. - Avoid primitive obsession: Important business values deserve their own types or records.
- Centralize mapping logic: RPG/Db2-to-Java conversions should not be scattered across controllers, services, and SQL helper classes.
- Build tests with boundary data: Test maximum packed values, negative amounts,
0.00,0.10, leading zeros, blank-padded text, umlauts, special characters, old status values, invalid data, and timestamps around DST transitions.
import static org.junit.jupiter.api.Assertions.assertEquals;
import java.math.BigDecimal;
import org.junit.jupiter.api.Test;
class MoneyTest {
@Test
void normalizesScaleToTwoDecimals() {
Money money = new Money(new BigDecimal("12.3"));
assertEquals(new BigDecimal("12.30"), money.amount());
}
@Test
void addsAmountsWithStableScale() {
Money result = new Money(new BigDecimal("0.10"))
.add(new Money(new BigDecimal("0.20")));
assertEquals(new BigDecimal("0.30"), result.amount());
assertEquals(2, result.amount().scale());
}
}
A good migration test does not only contain average values. It deliberately includes uncomfortable data: maximum packed values, negative amounts,
0.00, 0.10, leading zeros, empty and blank-filled text, umlauts, special characters, legacy status values, invalid data, and timestamps around daylight-saving transitions.Glossary
Brief explanations of central terms from RPG, Java, and IBM i modernization.
- Packed Decimal
- A compact decimal type in RPG, commonly used for monetary values with a defined number of digits and decimal places.
- BigDecimal
- Java class for immutable decimal numbers with arbitrary precision. Especially important for monetary amounts and exact decimal values.
- Zoned Decimal
- Decimal representation in which digits are stored in zoned format. Semantically this can become a number, an amount, a code, or a formatted value.
- CCSID
- Coded Character Set Identifier. On IBM i critical for character set interpretation, conversion, and correct text display.
- UTF-16 Code Unit
- 16-bit unit in Java’s character representation. A visible Unicode character may consist of one or more such units.
- LocalDate
- Java type representing a date without time and without timezone — for example a delivery date or birth date.
- Instant
- Java type representing a unique technical point on the timeline. Especially suitable for logs, events, and technical timestamps.
- Record
- Java language construct for compact, immutable data classes. Very practical for small Value Objects and DTOs.
- Primitive Type
- A built-in Java base type such as
int,long,double, orboolean. - Value Object
- A small business object that encapsulates a value together with its rules — for example a monetary amount, customer number, item number, or status.
Conclusion
RPG and Java can work together extremely well — but data types must be translated consciously.
The best counterpart is not always the most similar technical type, but the type that preserves business meaning.
For IBM i modernization this means: Packed Decimal usually belongs to BigDecimal, real dates to LocalDate, technical instants preferably to Instant, text to String with clean CCSID and Unicode awareness, and IDs with leading zeros should not automatically become numbers.
Teams that make these decisions deliberately avoid rounding errors, character-set dramas, broken keys, and unstable interfaces.
Sources & Further Reading
- IBM Documentation: Determining equivalent SQL and ILE RPG data types
- IBM Documentation: Numbers / decimal, packed and zoned data types
- IBM Documentation: Date, Time and Timestamp data types
- IBM Documentation: Variable-Length Character, Graphic and UCS-2 formats
- IBM Documentation: Character Data Type
- IBM Support: IBM Toolbox for Java JDBC Driver connection properties
- Oracle Java Tutorials: Primitive Data Types
- Oracle Java SE 21 API: BigDecimal
- Oracle Java SE 21 API: java.time Package Summary
- Oracle Java SE 21 API: Character and Unicode code points
- Oracle Java Language Guide: Records



tiny-tool.de
tiny-tool.de