Premium Guide · IBM i Modernization

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.

IBM RPG and Java data type comparison for IBM i modernization
RPG and Java data types are not just syntax — they shape precision, encoding, interfaces, and stable IBM i modernization.

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.

This article is written for RPG developers, Java developers, architects, testers, and anyone involved in placing Java services in front of existing IBM i data or gradually modernizing legacy RPG applications.
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.

The Golden Rule:
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.

Project Warning:
“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.

Technically possible — semantically risky:
double cannot represent many decimal values exactly. For monetary values BigDecimal is the safer, more robust choice.
JJava · Why double is imprecise for money
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.

Recommended approach:
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.
RRPG · Packed Decimal
**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;
JJava · BigDecimal with explicit scale
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.

JUnit · Boundary test for decimal mapping
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());
    }
}
Avoid this:
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.
Typical project mistake:
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.
JJava · Clean type selection for date/time
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();
JUnit · Securing time semantics
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.

CCSID is not a side issue:
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.
JJava · Code points instead of char counting
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);
JUnit · Preserving leading zeros in customer numbers
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.

Modernization tip:
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.

JJava · Money as a Record
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));
    }
}
JJava · Customer number as Value Object
public record CustomerNumber(String value) {

    public CustomerNumber {
        if (value == null || !value.matches("\\d{6}")) {
            throw new IllegalArgumentException("customer number must have exactly 6 digits");
        }
    }
}
JJava · Status as enum instead of anonymous flag
public enum OrderStatus {
    OPEN,
    RELEASED,
    SHIPPED,
    CANCELLED
}
Pragmatic rule of thumb:
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 BigDecimal for money, prices, and commercial values in Java.
  • Document scale explicitly: In packed(9:2) the :2 is business-critical, not decoration.
  • Model date and time cleanly: LocalDate for dates, Instant for technical instants, OffsetDateTime for 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, String or 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.
JUnit · Testing the Money record
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());
    }
}
Minimal test data set for migrations:
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, or boolean.
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

Transparency Notice


🔎
Transparency Notice:
The content on tiny-tool.de is carefully researched, editorially reviewed, and updated on a regular basis. Sources and quotations are provided in a transparent and traceable manner. However, we do not guarantee the accuracy, completeness, or timeliness of the information provided. Errors cannot be ruled out.

Authorship & Editorial Support: Texts published on tiny-tool.de are original editorial works created by our team (final editorial responsibility: Guido Zeuner). Digital tools — including AI-based assistance systems — may be used solely as support tools for research, structuring, or language refinement. The selection of content, structure, reasoning, and final wording are created and approved by us as natural persons; AI systems are not authors.

Audience Measurement (VG Wort / METIS): To measure text reach, tracking pixels provided by VG Wort are used. For technical reasons, these tracking marks are loaded when the page is accessed and currently cannot be blocked through the cookie banner, as no cookies are set. The measurement is used exclusively for reach statistics; no personal profiles are created. More information is available in our Privacy Notice.

Please note: The content provided on this website is for general informational purposes only and does not constitute professional advice of any kind, including legal, tax, financial, or other specialist advice. Use of the content is at your own risk. Liability for material or immaterial damages is excluded, except in cases of intentional misconduct or gross negligence.

Advertising & Affiliate Links: Some posts may contain promotional references or so-called affiliate links. These are identified accordingly. Clicking such links does not result in any additional cost to you; we may receive a small commission.

Trademark Notice: All brand names, logos, and product names are the property of their respective rights holders and are used solely for identification and descriptive purposes. No affiliation with the companies mentioned is implied.

External Links: This website contains links to external third-party websites. Although we review links carefully, we assume no responsibility for the content of external websites. If we become aware of unlawful content, we will remove the respective links without undue delay.