First a bit of terminology. The idea is that there is an "aggregate" in the domain-driven design sense of the word, i.e. an object graph that can be considered to be one composite "thing". For example, an Employee object might reference related Address records, and also reference another Employee object in the role of Supervisor. The Address records are part of the Employee aggregate, the Supervisor is not - it's an independent object that just happens to be referenced. The acid test is whether it would make sense to keep the record around if you deleted its parent. If you would always delete the child at the same time as the parent, the child is part of the parent's aggregate.
So, my code lets me stop serializing at the boundary of the aggregate - i.e. I could serialize just one Employee with all it's "own" data, no matter how many objects that data is spread across. References to things outside the aggregate are serialized as simple key values. Then when deserializing, you can look up the keys and wire everything back together again. This might be a more complex case than what you have in mind, but you can think of your case as a reductive one where the aggregate is always one table in size. Here's the full code for serializing: // This is the usual setup for serializing Hibernate stuff final XStream xstream = new XStream() { protected MapperWrapper wrapMapper(final MapperWrapper next) { return new HibernateMapper(next); } }; xstream.registerConverter(new HibernateProxyConverter()); xstream.registerConverter(new HibernatePersistentCollectionConverter(xstream.getMapper())); xstream.registerConverter(new HibernatePersistentMapConverter(xstream.getMapper())); xstream.registerConverter(new HibernatePersistentSortedMapConverter(xstream.getMapper())); xstream.registerConverter(new HibernatePersistentSortedSetConverter(xstream.getMapper())); // These are my custom converters xstream.registerConverter(new UnitExternalRefExportConverter()); xstream.registerConverter(new UnitPruningConverter(xstream.getMapper(), xstream.getReflectionProvider())); // I usually leave out internal database bookkeeping and audit data xstream.omitField(BaseDomainObject.class, "id"); xstream.omitField(BaseDomainObject.class, "createdDate"); xstream.omitField(BaseDomainObject.class, "modifiedDate"); xstream.omitField(BaseDomainObject.class, "modifiedBy"); return xstream.toXML(unit); "Unit" is the aggregate I'm serializing. I won't bother to explain its structure. BTW I'm still on the older version of XStream so I copied the Hibernate bits from the doc. I presume the rest of my approach still works in the new version. On to the interesting part, the custom converters. UnitExternalRefExportConverter looks like this: /* * Converts fields that are external to the unit aggregate root * and therefore should be represented by symbolic references * rather than included in toto */ public class UnitExternalRefExportConverter implements Converter { @Override public void marshal(Object obj, HierarchicalStreamWriter writer, MarshallingContext arg2) { BaseDomainObject bdo = (BaseDomainObject) obj; String textID = Utility.getTextIDValue(bdo, Utility.getTextKeyField(bdo.getClass())); writer.setValue(textID == null ? "" : textID); } @Override public Object unmarshal(HierarchicalStreamReader reader, UnmarshallingContext context) { throw new Error ("UnitExternalRefExportConverter should not be used for unmarshalling. Use UnitExternalRefImportConverter instead"); } @Override public boolean canConvert(Class clazz) { return (clazz.equals(Service.class) || clazz.equals(UnitType.class) || clazz.equals(Audience.class) || clazz.equals(LearningTeam.class) || clazz.equals(Leader.class) || clazz.equals(ContentPartner.class) || clazz.equals(User.class) || clazz.equals(AssessmentStrategy.class) || clazz.equals(Category.class) || clazz.equals(PDCollection.class) || clazz.equals(InteractionType.class) || clazz.equals(QuestionnaireFormat.class) || clazz.equals(QuestionnaireType.class) || clazz.equals(QuestionType.class) || clazz.equals(QuestionFormat.class) || clazz.equals(Validation.class) || clazz.equals(com.medeserv.mesquest.Service.class) ); } } The canConvert method just lists all of the classes that are external to the aggregate and should NOT be serialized. In your table by table scenario, this would be simply any class that is not the class mapped to the table you're serializing. The marshal method just writes a simple key value into the output stream (where the default converter would continue to recurse into the associated object). My code does some gymnastics to derive a database-independent textual key value here, as I often want to import the xml back into a different database, but within one database you could just use the foreign key. In terms of this code that would be obj.getId(). UnitPruningConverter is essentially a way to separate out user data from configuration data so I can serialize just the "static" parts of the object graph e.g. for copying a configuration from one server to another. /* * Prunes out parts of the tree that should not be serialized - primarily student data * We can't simply use @Transient to do this as that would then nobble Hibernate * Default unmarshalling is fine for this case - we'll always just have empty java collections */ public class UnitPruningConverter extends ReflectionConverter { public UnitPruningConverter(Mapper mapper, ReflectionProvider reflectionProvider) { super(mapper, reflectionProvider); } @Override protected void marshallField(MarshallingContext context, Object newObj, Field field) { if (field.getName().equals("collection")) { // Make sure these collections are always empty super.marshallField(context, "", field); } else { super.marshallField(context, newObj, field); } } @Override public boolean canConvert(Class clazz) { return (clazz.equals(UnitProgressManager.class) || clazz.equals(Unit_UserUnitHistoryManager.class) || clazz.equals(Unit_UserUnitAccessManager.class) || clazz.equals(Unit_LearningProjectUnitManager.class) || clazz.equals(Unit_UserNoteManager.class) || clazz.equals(InteractionProgressManager.class) || clazz.equals(InteractionAvailableManager.class) || clazz.equals(UnitAlertStatusManager.class) || clazz.equals(UserResponseInstanceManager.class) || clazz.equals(UserResponseManager.class) || clazz.equals(PocketDiscussionMessageManager.class) ); } } That's it. I know that's a little more complicated than the question you asked, but for me the notion of aggregates is powerful and useful, and without knowing your use case I didn't want to go too simple. However, we can simplify this to the case where the aggregate boundary is just the single table. As I understand your question, you'd like a converter that will automatically know when a field is outside the table, and use the foreign key to represent that object. Correct? I can think of a few ways to do that, but it depends on how your object model and your mappings are set up. If you map one table to one object (i.e. don't use component mappings), then your canConvert method just needs to check whether the object to be marshalled is a subclass of your base class. If it is, then it's external to the table, canConvert should return true, and your marshal method can then just write the foreign key to the xml stream. If, however, you use complex mappings, there are a couple of further options. Firstly, you could create a converter per table. Not as painful as it sounds as you could use the one class with a constructor parameter that tells it which table/object it is dealing with. I could jot down some pseudocode if you think this is the way to go. Secondly, you could inspect the Hibernate mapping data to work out which fields belong to which table. I won't go into how to do that here - personally I think that's complicated enough that you'd want to have a really compelling use case to make it worthwhile. Final point, just for completeness. Deserializing an aggregate is a little more complex. Converting the key values back into external object references generally requires a converter instance per external object class, as each external object may have a specific way to find it and then instantiate it if the desired value is missing. Having done that, the references for any bi-directional associations have to be re-created in the existing external objects. The complexity isn't crippling, but the code is highly specific to the exact structure of the aggregate being deserialized. Hope that helps! Jaime