Skip to content

Map format reference

A rosetta-frida map is a single strict-JSON file describing the real → obfuscated translation for one (app, version_code) pair. This page is the field-by-field reference. For the authoring workflow, see Authoring; for the on-bundle embedding, see Marker block.

Schema ownership

The canonical, language-neutral map schema is owned by the separate rosetta-maps repo (schema/rosetta-map.schema.json — the source of truth for the schema_version: 5 format). rosetta-frida is a client of that schema: its Zod validator (src/validate/schema.ts) tracks the canonical schema. This page documents the same format as consumed by the Frida client. rosetta-xposed (Kotlin) is the other client.

The canonical example lives at maps/com.example.app/30405.json in the repo. It exercises every feature documented here at least once — 15 classes covering AIDL stubs, callback proxies, value objects, an enum, a synthetic Companion, an anonymous inner class.

Top-level shape

interface RosettaMap {
    schema_version: 5;
    app: string;
    version: string;
    version_code: number;
    captured_at?: string; // ISO YYYY-MM-DD
    signer_sha256?: string | string[]; // single hash OR match-any array
    generated_from?: { signatures_rev: string };
    status?: 'active' | 'superseded' | 'retracted';
    superseded_by?: number;
    client_hints?: {
        frida_min_version?: string;
        frida_max_version?: string;
    };
    sources?: MapSource[];
    classes: ClassMap;
}

Required fields

Field Type Description
schema_version 4 The schema version. Must be 4. Bumped on breaking schema changes; old maps will fail to load against newer libraries until in-tree migrators are added. (4 made the map a pure real→obfuscated mapping: it removed the finding-evidence / AIDL fields no resolver read — methodEntry.aidl_txn, classEntry.aidl_descriptor, the classEntry.anchors array, and the aidl_stub / aidl_callback class kinds. 3 removed confidence, tightened captured_at to an ISO date, let signer_sha256 be an array, and added generated_from / status / superseded_by.)
app string Android package name (com.example.app). Cross-checked against the auto-detected app at session start.
version string App version label (PackageInfo.versionName, e.g. 3.4.5). A human display label only — NOT authoritative for selection (labels can repeat across builds). Used as the fuzzy-match fallback key.
version_code integer The authoritative app-identity key — the full Android longVersionCode ((versionCodeMajor << 32) | versionCode), never masked. The runtime selects maps by this first (O(1), monotonic per build); the version label is only a fallback. Capped at Number.MAX_SAFE_INTEGER (2^53 − 1) so the Frida JS client can represent it exactly.
classes object Real-FQN → ClassEntry. The whole point of the file.

Optional fields

Field Type Description
captured_at ISO date (YYYY-MM-DD) When the map was captured. Validated as a real calendar date — arbitrary text is rejected (schema 3, #39). Useful when reading old maps to know how stale they are.
signer_sha256 SHA-256 hex, or array of them Authenticity guard — the SHA-256 of the APK signing certificate (not the APK bytes). Either a single bare-lowercase 64-hex digest or a non-empty array of them; a live signer matching any entry passes (schema 3, #38 — covers key-rotation lineages). Cheap to verify on-device via PackageManager; guards against loading a map for a repackaged/spoofed app. Enforced at attach time: when present, rosetta.session(...) reads the live app's signing certificate in-process and fails closed (SignerMismatchError) on a mismatch. Opt out with enforceSigner: false. See API · Session.
generated_from object Provenance pointer back to the signatures revision this map was generated from (schema 3, #36). When present, signatures_rev (a 7–40-char git commit hash) is required.
status 'active' \| 'superseded' \| 'retracted' Lifecycle status (schema 3, #40). Absent ⇒ active. A superseded map still loads but rosetta.session(...) emits a map-status warning event; a retracted map is refused fail-closed (MapRetractedError).
superseded_by integer The version_code of the map that replaces this one. Only meaningful alongside status: 'superseded' (or as a hint on a retracted map).
client_hints object Per-client metadata (its own keys are strict). Frida reads client_hints.frida_min_version / client_hints.frida_max_version (semver) — the Frida runtime range this map is known to work with. Not enforced at runtime in V1; emitted as metadata.
sources MapSource[] Provenance per tool. See Provenance.

Provenance — sources

A real map for a non-trivial app comes from several tools. The format tracks which entries came from where:

"sources": [
    {
        "tool": "sigmatcher",
        "config": "signatures/example.json",
        "classes": 10
    },
    {
        "tool": "hand-authored",
        "classes": 5
    }
]
interface MapSource {
    tool: string;
    config?: string;
    classes?: number;
}

Each class entry can carry a source: <tool> field that cross-references one of these entries — see ClassEntry.source.

Field Description
tool Free-form. Common values: sigmatcher, hand-authored, rosetta-runtime-discovered (V2+), jadx.
config The config or config-path the tool was run with.
classes Count of classes attributed to this source.
notes Free-form notes about the capture session.

Classes — ClassEntry

A class entry is keyed by its real fully-qualified name:

"classes": {
    "com.example.app.IRemoteService$Stub": {
        "obfuscated": "aaaa",
        "extends": "android.os.Binder",
        "kind": "class",
        "dex": "classes6.dex",
        "source": "sigmatcher",
        "methods": { /* ... */ },
        "fields": { /* ... */ }
    }
}

ClassEntry fields

interface ClassEntry {
    obfuscated: string;
    extends?: string;
    kind?: ClassKind;
    dex?: string;
    methods?: MethodMap;
    fields?: FieldMap;
    source?: string;
}

type ClassKind = 'class' | 'interface' | 'enum' | 'synthetic' | 'anonymous';
Field Description
obfuscated The obfuscated short name (aaaa, bbbb). The whole point of the entry.
extends Parent class. Either a real name (must also be a key in classes) or an obfuscated name (for parents like java.lang.Object or framework helpers we don't have a real-name mapping for).
kind What kind of class this is. Drives V2+ runtime discovery — e.g. synthetic classes get skipped by adaptive strategies.
dex DEX shard the class lives in (classes6.dex). Optional debugging metadata.
methods Methods keyed by real name. See Methods.
fields Fields keyed by real name. See Fields.
source Cross-reference into top-level sources — which tool contributed this entry.

Methods

Methods are keyed by real method name. The value is either a single MethodEntry (the common case) or an array (when one real name has multiple overloads).

Single-overload form

"methods": {
    "requestPrompt": {
        "obfuscated": "f",
        "signature": "(Landroid/os/Bundle;Lcccc;)V"
    },
    "isComplete": {
        "obfuscated": "e",
        "signature": "()Z"
    }
}

Overload-array form

"methods": {
    "requestTicket": [
        {
            "obfuscated": "c",
            "signature": "(Landroid/os/Bundle;Lbbbb;)V"
        },
        {
            "obfuscated": "d",
            "signature": "(Landroid/os/Bundle;Ljava/lang/String;Lbbbb;)V"
        }
    ]
}

MethodEntry fields

interface MethodEntry {
    obfuscated: string;
    signature: string;
    static?: boolean;
    synthetic?: boolean;
    is_constructor?: boolean;
}

type MethodMap = Record<string, MethodEntry | MethodEntry[]>;
Field Description
obfuscated Obfuscated method name (c, f). Some methods keep their real names (onTransact, values, valueOf) — that's fine, just use the same string.
signature JVM descriptor with obfuscated class refs in L...; positions. Example: (Landroid/os/Bundle;Lbbbb;)V. The resolver parses this to translate .overload(...) calls.
static Whether the method is static.
synthetic Whether the method is compiler-generated (e.g. bridge methods).
is_constructor true for <init> entries — constructors.

Constructors

Constructors are written with the real name <init> and the obfuscated name also <init>. Frida exposes them the same way:

"<init>": [
    {
        "obfuscated": "<init>",
        "signature": "(Ljava/lang/String;)V",
        "is_constructor": true
    },
    {
        "obfuscated": "<init>",
        "signature": "(Ljava/lang/String;J)V",
        "is_constructor": true
    }
]

Fields

Fields are keyed by real field name:

"fields": {
    "sessionId": {
        "obfuscated": "a",
        "type": "Ljava/lang/String;"
    },
    "MAX_SIZE": {
        "obfuscated": "b",
        "type": "I",
        "static": true
    }
}
interface FieldEntry {
    obfuscated: string;
    type: string;
    static?: boolean;
}

type FieldMap = Record<string, FieldEntry>;
Field Description
obfuscated Obfuscated field name.
type JVM descriptor for the field type. Primitives: I, J, Z, B, C, S, F, D. Class refs: L<obfClassName>; — the on-disk type uses obfuscated class refs, just like method signatures.
static Whether the field is static. Static fields are accessed via the class proxy (Klass.STATIC_FIELD.value); instance fields via rosetta.field(...) on an instance.

Cross-class type references

When a method signature mentions another mapped class ((Landroid/os/Bundle;Lbbbb;)Vbbbb is com.example.app.IServiceCallback), the Resolver builds a reverse index at load time. This lets the proxy layer translate .overload('com.example.app.IServiceCallback', ...) into .overload('bbbb', ...) automatically.

The on-disk format always uses obfuscated class refs in signatures and field types — they're what the underlying Frida runtime ultimately needs.

Multi-version registry

For a single bundle that ships maps for many versions:

type RosettaMapRegistry = Record<string, RosettaMap>;
{
    "3.4.5": {
        "schema_version": 5,
        "app": "com.example.app",
        "version": "3.4.5",
        "version_code": 30405,
        "classes": {}
    },
    "3.4.6": {
        "schema_version": 5,
        "app": "com.example.app",
        "version": "3.4.6",
        "version_code": 30406,
        "classes": {}
    }
}

The registry is keyed by version label for human readability, but the session selects the right entry by the detected version_code first (scanning the entries for a matching code), falling back to the label only when no code is available or matches. With versionMatch: 'exact', a non-matching build throws; with 'fuzzy', the closest label wins.

See Multi-version bundle recipe for the full workflow.

Loading maps — loadMap

import { loadMap } from 'rosetta-frida';

const map = await loadMap('./maps/com.example.app/30405.json');

loadMap accepts:

  • A RosettaMap (passed through the validator and returned).
  • A strict-JSON source string (parsed, then validated). Comments and trailing commas are rejected.
  • A filesystem path (read, parsed, validated).

The path-vs-source heuristic is cheap: if the first non-whitespace character looks like JSON ({, [, ", digit, …) the string is treated as source. Otherwise it's a path.

In the Frida runtime (where there is no filesystem), loadMap is typically not needed — the map reaches your hook via import map from './x.json' at compile time. loadMap shines on the CLI side and in tests.

Validation

Every map flows through a Zod schema. Authoring tools (the CLI, loadMap, yamlToMap) all run the same validator, so format errors surface uniformly:

FAIL: maps/com.example.app/30405.json — invalid map
  at classes.com.example.app.IRemoteService$Stub.obfuscated: required
  at classes.com.example.app.Foo.methods.bar.signature: must match /\(.*\)[^()]+/

The Zod schema is exported from the package as rosettaMapSchema for callers that want to validate ad-hoc objects:

import { rosettaMapSchema, MapValidationError } from 'rosetta-frida';

const result = rosettaMapSchema.safeParse(maybeMap);
if (!result.success) {
    throw new MapValidationError(
        'failed validation',
        result.error.issues.map((i) => ({ path: i.path.join('.'), message: i.message })),
    );
}

See Errors — MapValidationError for the error shape.

Validation limits (security bounds)

A map is untrusted input. To stop a hostile or corrupt map from driving unbounded work in the resolver — or shadowing object internals via crafted record keys — the validator enforces hard caps that mirror the canonical JSON Schema in rosetta-maps (schema/rosetta-map.schema.json). A map that exceeds any cap fails validation up front rather than at attach time.

Bound Limit
classes entries 50 000
methods per class 5 000
fields per class 5 000
method overloads (array) 200 (min 1)
sources 100
version_code 9 007 199 254 740 991 (Number.MAX_SAFE_INTEGER, 2^53 − 1 — the full longVersionCode)
obfuscated / short names 512 chars
extends 4 096 chars (free-form / possibly-FQN type name)
signature / field type 4 096 chars
app 256 chars
version 256 chars
any other free-form string 4 096 chars

Two additional shape constraints:

  • app must be a dotted package name (^[A-Za-z][A-Za-z0-9_]*(\.[A-Za-z0-9_]+)+$).
  • signer_sha256 (when present) is either a single 64-lowercase-hex string (^[0-9a-f]{64}$) or a non-empty array of such strings (match-any). The map value is always bare lowercase hex — no colons/uppercase; the session layer normalises the live app-presented hash (strip :, lowercase) before comparison.
  • generated_from.signatures_rev (when present) must be a 7–40-char lowercase-hex git commit hash (^[0-9a-f]{7,40}$).
  • status (when present) is one of active / superseded / retracted; superseded_by is a non-negative integer version_code.

Reserved keys. The keys __proto__, constructor, and prototype are rejected anywhere they appear in a classes, methods, or fields object. (JSON.parse produces a genuine own __proto__ key, so this is checked against the raw input.) This blocks prototype-pollution / bracket-index footguns in the resolver; the whole map is rejected rather than silently sanitised.

Schema evolution

schema_version is mandatory; missing-version maps fail to load.

New optional fields are additive: old maps continue to load against newer libraries (the library just sees undefined for the new fields).

Breaking changes bump schema_version. The current schema is 4 (it made the map a pure real→obfuscated mapping by removing the finding-evidence / AIDL fields no resolver read — methodEntry.aidl_txn, classEntry.aidl_descriptor, the classEntry.anchors array, and the aidl_stub / aidl_callback class kinds — which now live only in the signatures authoring source); schema_version: 3 (and 2 / 1) maps fail to load and must be re-emitted at version 4. Future breaking changes will ship in-tree migrators (4 → 5, ...) so old maps keep loading after migration.

Bumping the schema version (maintainers)

The version lives in one place — CURRENT_SCHEMA_VERSION in src/types/map.ts — which drives the RosettaMap.schema_version type, the Zod gate, the adapter, and rosetta init. To bump it:

  1. Change CURRENT_SCHEMA_VERSION.
  2. Run npm run schema-version:fix to update the literals in the docs and the canonical sample map, then review the diff.
  3. Update any negative-test fixtures by hand (the suite deliberately carries invalid/old versions for rejection tests).

npm run schema-version:check (part of npm run verify and CI) fails if a doc/sample literal drifts from the constant. Mark an intentional older literal inside a code block with a schema-keep comment to exempt it.