Home / Blog / Migration & Interoperability

Exporting Architecture Back to Sparx EA Without Duplicating Objects

Import a Sparx Enterprise Architect model into almost any external tool, make a change, and export it back, and you will usually get a repository with twice as many objects as it started with. Not because the tool is badly built in some obvious way, but because round-trip export is a harder problem than it looks, and most pipelines quietly fail at the one part that matters: telling the deserializer on the other end "this already exists, update it" instead of "here is something new."

This is the single most common way an EA-to-external-tool workflow breaks trust. The first import goes fine. The first export goes fine. Then someone re-imports the exported file into the same repository to bring the edits back, and the repository browser shows two "Payment Gateway" application components where there used to be one, both with slightly different property values, neither of which anyone can now safely delete without checking what's still connected to it. After that happens once, the tool stops being used for anything that touches a live repository. It gets demoted to a one-way export target — pretty pictures out, nothing useful back in — which defeats most of the reason for having an external editor in the first place.

Why duplication is the default outcome, not an edge case

It helps to be precise about why this happens, because the failure isn't exotic. It's the direct consequence of a design choice that looks harmless in isolation.

A Sparx EA repository identifies every object — package, element, connector, attribute — by a GUID it assigns at creation time. That GUID is the object's real identity. The name, the type, the notes field, the tagged values: all of that can change over the object's lifetime without affecting which object it is. The GUID is what Sparx actually checks when it matches an incoming XMI or CSV import against something already in the repository.

Most export pipelines never look at that GUID, or look at it and then throw it away. The typical shape is: read the objects out of Sparx via the API or an XMI export, hand them to the external tool as plain data (name, type, a few attributes), let someone edit them, and on the way back out mint a fresh identifier for each object before handing the file to Sparx's importer. From the importer's point of view, every single object in that file is new, because as far as it can tell, it is new — nothing in the payload points back to anything already in the repository. It does exactly what it's told and creates a full duplicate set. Run the same cycle a second time and you get a third copy. This isn't a Sparx bug. The importer is behaving correctly given the input it was handed; the input just never carried the one fact that would have let it behave differently.

The fix isn't clever reconciliation logic bolted onto the Sparx side after the fact — matching by name, by type, by some fuzzy similarity score, and hoping nothing collides. It's much simpler than that, and it has to happen upstream, at export time: don't discard the identity you already have.

Identity-preserving export: reuse the GUID, don't mint a new one

The mechanism Mooodels uses is deliberately unglamorous. When an element is imported from Sparx into Mooodels, its original EA GUID is captured and stored alongside it as part of the element's source mapping — not as a display property, as a piece of provenance the element carries silently through however many edits happen next. Rename it, move it, attach new relationships, change its properties: the GUID doesn't move, because it was never derived from any of those things. It's the one field on the object that Mooodels itself never regenerates.

When that element is exported back out, the export carries that stored GUID along with it — the same identifier, verbatim, that Sparx originally assigned it, rather than a freshly generated one. Nothing about the export needs to be exotic for this to work; the identity travelling with the element just needs to be the real one. On the Sparx side, the deserializer script that reads the file back in does GUID matching as its first step, the same way Sparx's own import routines do: does an object with this GUID already exist in the repository? If yes, update its fields in place — new name, new notes, new tagged values, whatever changed. If no, create it. That's the entire mechanism. There's no fuzzy matching, no "looks like the same thing" heuristic, no dependency on names staying stable between edits. A GUID either matches an existing repository object or it doesn't, and that binary answer is exactly what determines update versus create.

Naive export mints a new ID every time and duplicates; identity-preserving export reuses the original GUID and updates in place Naive export Identity-preserving export Sparx object · GUID A1 import, edit, export Exported object · new GUID B2 re-import GUID A1 (untouched) GUID B2 (new copy) Result: duplicate object every re-import doubles the count Sparx object · GUID A1 import, edit, export Exported object · same GUID A1 re-import GUID A1 matched, updated Result: one object, edits applied repeat the cycle indefinitely, count stays the same
The only difference between duplication and a clean update is whether the export token is a freshly minted ID or the object's original Sparx GUID.

It's worth being explicit about what this does and doesn't require. It doesn't require Sparx to run any special plugin, or the repository to be in any particular state, or the architect to remember to do anything manually. It requires exactly one thing: that the external tool never throws away the identity it was handed on import. Everything downstream — matching, updating in place, leaving unrelated objects untouched — falls out of that one decision. This is also why bolting the fix on after the fact, as a reconciliation pass that tries to guess which exported object corresponds to which repository object by name or type, is the wrong shape of solution even when it mostly works. It's solving, with inference, a problem that shouldn't need inference — the answer was available for free at import time and just needs to be carried through.

Walking through a real round trip

The abstract version of this is easy to nod along to. The concrete version is what actually builds confidence, so it's worth walking through a full cycle the way it plays out on a real repository.

Start with a small slice of an existing Sparx model — say an application layer with three components and a couple of realizations to business services — and import it into Mooodels once.

  1. Import. The selected package comes out of the Sparx repository and lands in Mooodels as three elements and two relationships, each one still carrying the EA GUID it already had, recorded as source mapping rather than as anything the architect has to see or manage.
  2. Edit inside Mooodels. Rename "Payment Gateway" to "Payment Gateway v2", add a new relationship from it to Fraud Check API, and delete the "Legacy Billing Adapter" element entirely.
  3. Export. Payment Gateway v2 and Fraud Check API both go back out carrying the original GUIDs Sparx assigned them. The new relationship goes out with a freshly minted one, because it never existed in Sparx and there is nothing to reuse. Legacy Billing Adapter is simply not in the export at all.
  4. Re-import into the same repository. GUID matching finds Payment Gateway v2 and updates its name in place, finds Fraud Check API and updates its fields, doesn't find the new relationship and so creates it, and leaves Legacy Billing Adapter exactly as it was — because an object that wasn't in the export is not an instruction to delete anything. Export updates and adds; it isn't a sync that removes whatever it didn't see.
  5. Confirm. The repository browser shows one Payment Gateway v2, with the GUID it always had, in the package it always sat in, keeping every relationship it already had plus the new one. No duplicate objects anywhere in the package. Legacy Billing Adapter is still there in Sparx, because deleting it in Mooodels doesn't delete it in Sparx unless that's handled as an explicit, separate step.

Two things in that sequence are worth pulling out because they're easy to miss on a first read. First, the new relationship in step 3 gets a freshly minted identifier, not because the mechanism failed, but because there's nothing to reuse — it never existed in Sparx, so it's genuinely new, and the deserializer correctly creates it rather than matching it to anything. Identity preservation only applies to identity that already exists; it doesn't invent continuity where there isn't any. Second, deleting something in Mooodels doesn't propagate as a delete in Sparx by default. An object simply absent from an export file is not evidence that it should be removed from the repository — it might just be outside the scope of what was selected for export this time. Whether a deletion in the external model should also delete the Sparx object is a deliberate, separate decision, not a side effect of the object failing to appear in one export.

What happens to elements that never came from Sparx

Not everything in a Mooodels model necessarily originated in Sparx. An architect might import an existing landscape, then add three entirely new components while sketching a target-state design, then export the whole thing back. Those three new components have no EA GUID to reuse, because they were never Sparx objects to begin with — there's no identity to preserve.

For elements like that, export mints a fresh identifier, the same way it would for any brand-new object, and the Sparx-side importer treats them exactly as new objects should be treated: created fresh, added to the repository. Where they land matters and is worth being deliberate about. An element created in Mooodels with no Sparx origin and no explicit folder assignment lands at the model root on import — which works, but produces a package tree that doesn't reflect anyone's actual filing intent. Assigning a target folder before export avoids that; it's a small step that saves a cleanup pass in the repository browser afterward. This is really the same principle as the GUID reuse, one level up: the tool gives you a sensible, honest default when it doesn't have enough information to be more specific, rather than guessing at a folder location it has no basis for.

Relationships that lose an endpoint

A quieter failure mode in naive round-trip tools is the dangling reference: a relationship gets exported pointing at an element that no longer exists, because the element was deleted from the model after import but the relationship pointing to it wasn't cleaned up alongside it. Sparx's importer, handed a connector whose source or target GUID doesn't resolve to anything, has to do something with that — and "something" is rarely graceful. Depending on the exact import path it can throw an error and abort the whole batch, or silently create a placeholder object to satisfy the reference, which is arguably worse than an outright failure because it looks like a successful import and leaves debris in the repository.

Mooodels avoids this by construction rather than by cleanup. If a relationship's endpoint was deleted from the model since import, that relationship is simply dropped from the export — not exported with a broken reference, not exported pointing at a placeholder. The export step checks that both endpoints of every relationship still resolve to something in the model being exported before including it, and anything that fails that check is left out, with a note in the export log so it isn't a silent omission. The repository never sees the broken reference in the first place, because it's never generated.

The one case export can't handle: a relationship whose endpoints are themselves both relationships. This is legal under ArchiMate 3.1 — relationships can attach to other relationships, not just to elements — but Sparx's connector model doesn't support a connector whose source or target is another connector. There's no EA construct to export it into. Mooodels detects this on export the same way it detects it on import, skips the relationship, and reports it as a warning rather than either dropping it silently or forcing a workaround representation that Sparx would just reject.

Why the relationship-to-relationship case is a real limit, not a rounding error

It's worth explaining why that specific case gets called out rather than folded into general "some things don't round-trip perfectly" language, because the reason is structural, not a gap in effort. ArchiMate 3.1 introduced the ability for a relationship to serve as the source or target of another relationship — most commonly to let an influence or association relationship say something about a dependency itself, rather than about the two elements the dependency connects. It's a legitimate modelling need, and Mooodels' Generic and ArchiMate profiles both support representing it.

Sparx EA's underlying object model, though, represents a connector as always running between two elements. There is no connector-to-connector construct in the schema the importer reads into, so there's no honest way to express "this relationship's target is that other relationship" in an XMI file destined for Sparx — not because of a missing feature in the export code, but because the destination format has no slot for that fact to live in. Any attempt to force it through would mean inventing a workaround representation on export — a synthetic intermediate element standing in for the relationship, say — that Sparx would treat as real and that would need to be reverse-engineered back out on the next import, which is exactly the kind of silent, lossy transformation that erodes trust in round-tripping faster than an honest skip does. A visible warning that names the specific relationship and explains why it was left out is more useful than either a silent drop or a technically-present-but-wrong substitute, because it tells the architect exactly what needs a different treatment — modelled as two separate relationships to the shared elements instead, for instance — rather than leaving them to discover the gap later by noticing something's missing.

This mirrors the same limitation on the way in: importing a Sparx model that happens to already contain something Sparx itself shouldn't be able to produce isn't a scenario that comes up, precisely because Sparx never generates connector-to-connector relationships in the first place. The constraint is symmetric because it's rooted in what the destination format can represent, not in which direction data happens to be moving.

Edge cases worth asking about directly

A handful of related questions come up whenever this workflow gets scrutinized properly, and they're worth answering explicitly rather than leaving implied.

What if the same repository object was also changed in Sparx during the round trip?

This is the classic concurrent-edit problem, and identity preservation doesn't magically solve it — it just makes it visible in the right place. If someone edits "Payment Gateway v2" directly in Sparx while the same element is also being edited in Mooodels, then on re-import, GUID matching finds the object and applies the incoming update, which will overwrite whatever changed on the Sparx side in the fields the export touches. That's not a flaw specific to this mechanism; it's true of any update-in-place system, including Sparx's own multi-user repository locking. The practical answer is the same one any team already applies to a shared repository: treat the package being round-tripped as checked out to one workflow at a time, the same discipline already required for two humans editing the same Sparx package concurrently. GUID-based matching doesn't need a merge algorithm to behave correctly — it needs the same edit-ownership convention any shared repository needs.

Does moving an element to a different package in Sparx survive the round trip?

Package location is stored against the GUID like everything else, so if an element is moved in Sparx after import, that new location is what the next import into Mooodels will reflect the next time it's pulled in — Mooodels doesn't need to know a move happened; it just sees where the object currently lives. Going the other direction, if an element is reassigned to a different folder inside Mooodels, export carries that folder assignment along with the update, and the deserializer relocates the existing repository object rather than leaving a stale copy behind in its old package. Because the operation is still a GUID-matched update rather than a delete-and-recreate, the object's relationships and history survive the move on either side.

What happens to a Sparx-specific field Mooodels doesn't have a slot for?

Sparx repositories, especially ones customized with MDG technologies or bespoke tagged value profiles, often carry fields that don't map cleanly onto a generic or ArchiMate profile. Fields Mooodels doesn't recognize on import are preserved as opaque metadata on the element rather than discarded, and are written back unchanged on export unless something in Mooodels explicitly edited them. The practical implication is that round-tripping through Mooodels doesn't require the destination profile to model every custom field a Sparx repository happens to carry — only the fields actually being edited need to be understood by the tool doing the editing.

What good round-trip behavior earns you

None of this is about round-tripping being an interesting technical puzzle for its own sake. It's about what becomes possible once an architect can trust that a cycle of import, edit, export, re-import behaves the way the workflow above described — and, just as importantly, what becomes impossible to trust when it doesn't.

With identity-preserving export, editing in Mooodels stops being a one-way trip. An architect can pull a slice of a live Sparx repository into a faster, more visual, or more AI-assisted editing environment, make substantive changes — rename things, restructure relationships, delete what's obsolete, add what's missing — and push those changes back into the repository people actually govern and report from, repeatedly, without the repository accumulating cruft on every cycle. That's what makes it reasonable to treat an external tool as a real editing surface for a governed model rather than a one-time import convenience or a permanent fork that the canonical repository has to be manually reconciled against later.

Without it, every external tool in this category ends up in the same place regardless of how good its editing experience is: useful for a single import, useful for producing a diagram or a report, and quietly untrustworthy the moment someone tries to bring changes back. Getting identity preservation right is not a nice-to-have polish item bolted onto import/export. It's the difference between a round trip and a one-way trip that happens to go through a return flight.

See the model this article describes, working in a real editor.

Try the live demo