A possible work-around could be the class below. In short, you work with your json property as intended, merely before saving the object, you save the json-property into a stream and after opening an instance, you restore the json-property from the the stream - that's all. The drawback, no SQL over the json property...

Class DC.Dyn Extends %Persistent
{
Property json As %DynamicObject [ Transient ];
Property jstr As %GlobalCharacterStream [ Internal, Private ];

ClassMethod MyTest(kill = 0)
{
   if kill do ..%KillExtent(1,1)

   set obj=..%New()
   set obj.json.short="A short test text"
   set obj.json.maxstr=$tr($j("",$$$MaxStringLength)," ","X")
   do obj.json.%Set("hugedata",..stream(obj),"stream")

   write "Status : ",obj.%Save(),!
   set id=obj.%Id()
   write "ID : ",id,!
   kill (id)

   set obj=..%OpenId(id)
   write "short : ",obj.json.short,!
   write "maxstr : ",$e(obj.json.maxstr,1,20),"... Size: ",$length(obj.json.maxstr),!
   set stream=obj.json.%Get("hugedata",,"stream")
   write "hugedata: ",stream.Read(20),"... Size: ",stream.Size,!
}

ClassMethod stream(obj)
{
   set stream=##class(%Stream.TmpCharacter).%New()
   do stream.Write(obj.json.short)
   do stream.Write(obj.json.maxstr)
   do stream.Write(obj.json.maxstr)
   quit stream
}

Method %OnOpen() As %Status [ Private, ServerOnly = 1 ]
{
   if ..jstr {
      do ..jstr.Rewind()
      set ..json=##class(%DynamicAbstractObject).%FromJSON(..jstr)
   }
   Quit $$$OK
}

Method %OnAddToSaveSet(depth As %Integer = 3, insert As %Integer = 0, callcount As %Integer = 0) As %Status [ Private, ServerOnly = 1 ]
{
   do ..jstr.Clear(), ..json.%ToJSON(..jstr)
   Quit $$$OK
}
}

Some testing...

IDEV:USER>d ##class(DC.Dyn).MyTest(1)
Status  : 1
ID      : 1
short   : A short test text
maxstr  : XXXXXXXXXXXXXXXXXXXX... Size: 3641144
hugedata: A short test textXXX... Size: 7282305

If your code uses obj.%Reload() then %OnReload() and %OnOpen() should contain the same code.

Your solution is just perfect. And fast.

But yes, you can avoid string manipulations... This one, for example, uses math only, merely it's neither short nor looks elegant:

 set dt=$h write $zd(dt,8)*100+($p(dt,",",2)\3600)*100+($p(dt,",",2)#3600\60)*100+($p(dt,",",2)#60)

but gives the same result as your short and nice solution...

 set dt=$h write $zd(dt,8)*100+($p(dt,",",2)\3600)*100+($p(dt,",",2)#3600\60)*100+($p(dt,",",2)#60),!,$tr($zdt(dt,8)," :")

On the other hand, you can install new brakes on your car, as suggested by others... ;-))

Just compare those codes with yours:

set h=$h, t=$zh for i=1:1:1E6 { set x=$tr($system.SQL.TOCHAR($h,"YYYY^MM^DD^HH24^MI^SS"),"^") } write $zh-t,!

set h=$h, t=$zh for i=1:1:1E6 { set x=$tr($zdt(h,8)," :") } write $zh-t,!

The choice is yours...

Just to put things in right perspektive, those "one letter commands" and "a lot of them in the same line" were neither tempting nor addictive, they were simply necessity!

At the time of the birth of MUMPS (the core of Cache/IRIS/etc.), more then 50 years ago in the second half of 1960es,  memory (which was a real core memory at the time) was rare and expensive and was measured in units of kilobytes! Just to contrast, today's server have the same amount of RAM, but in gigabytes, that's a factor of one million!

As a consequence of memory shortage and because MUMPS of that time was interpreted (i.e. you loaded the sourcecode into memory), one had to utilize each and every possibility to save memory. One of those possibilities were the ability of the language to short each command to one letter and to put as many commads as possible into one line (thereby saving line-ending bytes).

The tools (to save memory) of that (ancient) time were argumentless IFs and ELSES, short (variable-, global- and routine) names, commands with postcondition and sophisticated programming.

Last but not least, if one aims to "modernize" thos old applications, should be keept in mind, especially, if one is not so familiar with the old fashioned style and methods, there will be many unexpected pitfalls.

Sample1: on old printers, the line with "Total..." will be printed "bold-alike"

 write "last item",?15,$j($fn(val,",",2),10),!
 write ?15,"----------",! do  do  do
 . write $c(13),"Total",?15,$j($fn(sum,",",2),10)
 write !!!,"Due date for payment ....",!

Sample2: converting from:

 ; normal flow
 do
 . ; nested
 . ; commands
 ; normal flow

into:

 ; normal flow
 if 1 {
   ; nestd
   ; commands
 }
 ; normal flow

will be in most cases OK, except, if the nested part uses the current value of $STACK:
this is now one less then in case of argumentless DO!

According to documentation,  the tformat paramer 5 is ignored:

"Specify time in the form "hh:mm:ss+/-hh:mm" (24-hour clock). The time is specified as local time. The following optional suffix may be supplied, but is ignored: a plus (+) or minus (–) suffix followed by the offset of local time from Coordinated Universal Time (UTC). A minus sign (-hh:mm) indicates that the local time is earlier (westward) of the Greenwich meridian by the returned offset number of hours and minutes. A plus sign (+hh:mm) indicates that the local time is later (eastward) of the Greenwich meridian by the returned offset number of hours and minutes."

The same goes for the parameter values 6, 7 and 8

write $zdth("2021-11-04T11:10:00+0100",3,5)  --> 66052,40200
write $zdth("2021-11-04T11:10:00+0200",3,5)  --> 66052,40200
write $zdth("2021-11-04T11:10:00-0100",3,5)  --> 66052,40200

OK, I start with the second question. I'm not aware of a function to see if a specific global is in a buffer or not but there is a routine which shows which globals are using the most buffers:

znspace "%SYS"
do ^GLOBUFF

For the first question: if a global is used continuously, then it will always be in buffer. That's the simple answer. The reality depends on many other factors: the size of the global, the size of the buffer pool, how many other globals are in use, how often is a global used, etc.

To keep a few specific global(s) always in a buffer, there is a simple trick (assuming, your Cache/IRIS installation uses the default setup and you have an unused block size):

1) Goto SystemAdministration-->Configuration-->AdditionalSettings-->Startup: and edit the DBSizesAllowed setting, by checking one of the 16K or the 32K checkboxes

2) Create a new database with the newly enabled block size. This database will hold those few (always needed) globals.

3) Goto SystemAdministration-->Configuration-->SystemConfiguration-->MemoryAndStartup: and allocate (plenty of) memory for the newly created buffersize. Please consider,  after this chanhe, you have to RESTART your system!

4) Copy the global(s) in question into the newly created database:

  merge ^|"^^c:\path_to_new_database\"|GlobalName = ^|"^^c:\path_to_old_database\"|GlobalName

5) Create a Global mapping for the globals in question to the new location.

6) Start working... If everything is OK (which should be) and you are happy, delete the old global data to free up database space:   

kill merge ^|"^^c:\path_to_old_database\"|GlobalName

7) In a standard installation, you have allocated  one buffer pool (with the standard 8KB buffer size). So all your processes faiting to get the needed globals into that buffer pool.

With the above configuration you have two buffer pools, one for the standard 8KB database blocks and one for the new 16KB (or wahtever size you have choosen) database blocks. So you can keep important globals in a separate buffer pool. If you can manage (this will be application dependent) to give this buffer pool the same size as the database itself, the you will have all data (of this database) in the memory all day long.

I fear  you have to be some kind of a magician, to solve this problem...
You need  two things (a) a time-zone-offset, which is not the problem (it's more or less static) and (b) DST-offset, which is a problem, because there are databases for the past but not for future. Maybe you can put the DST-offset into a global for each of the geographic region you need. And yes, you have to maintain it...

Some starting points:  https://en.wikipedia.org/wiki/Tz_database  and http://web.cs.ucla.edu/~eggert/tz/tz-link.htm. In case you you work with python, take a look at https://pypi.org/project/pytz/

The README file from tz_database says the problem in a nutshell:
"The Time Zone Database (called tz, tzdb or zoneinfo) contains code and data that represent the history of local time for many representative locations around the globe.  It is updated periodically to reflect changes made by political bodies to time zone boundaries, UTC offsets, and daylight-saving rules."

You have a problem with the scoping!

Indirection has a global scoping, you have to put things with indirection in a global scope:

ClassMethod testvalidator(class As %String, value As %String) As %Status [ PublicList = (validator, sc) ]
{
   new validator, sc
   set validator = "sc = ##class("_class_").IsValid("""_value_""")"
   write validator,!
   set @validator
   write sc,!
   quit sc
}
set result = ##class(...).testvalidator("%Library.Numeric","BLABLA")
do $system.OBJ.DisplayError(result) --> ERROR #7207: Datatype value 'BLABLA' is not a valid number

There is one thing you should check, than this could trigger effects observed by you.
Objects are tracked by reference counts, as long as an objects reference count is greater then one, locks won't be released and the object isn't deleted.

set obj = ##class(Some.Class).%OpenId(id, 4) // the obj's ref count is one
... // more commands
    // now, the application does something like this
set tmp = obj    // obj's ref count is now two!
... // more commands

set obj = "" // the application intents to close the object
             // but the object still exists due to the fact that the ref count is one
             // (the object is still referenced by <tmp>)

There are methods to detect such a situation:
- $system.OBJ.ShowObjects(), lists all objects with reference counts
- $system.OBJ.ShowReferences(obj), list all variables which contains a reference to <obj>

A quick and dirty approach:

set filename = "...some file name"
open filename:"nw":0
if $t { use filename
        do $system.ShowObjects()
        do $system.ShowReferences(obj) 
        close filename
      }
set obj = "" 

Give it a try, maybe your object has multiple references which cause the problem

The %SYS.Namespace class contains the methods, you are looking for.

write ##class(%SYS.Namespace).GetGlobalDest( [namspace], "global") --> DB where the global lies
write ##class(%SYS.Namespace).GetRoutineDest( [namspace], "routine") --> DB where the routine lies
write ##class(%SYS.Namespace).GetPackageDest( [namspace], "package") --> DB where the package lies
set old = ##class(%Stream.TmpCharacter).%New()
do old.Write("This is my text")

So, now you have an old stream, "This is my text" but want to have a new stream as "This is my NEW text".

set new = ##class(%Stream.TmpCharacter).%New()
do old.Rewind()
set pos = 10 // This is my
do new.Write(old.Read(pos)), new.Write(" NEW"), new.Write(old.Read(old.Size-pos))

And now, check the resulty

do new.Rewind()
write new.Read(new.Size) --> This is my NEW text

<Nitpicking ON>

For the LAST() function you can also use just an expression:

LAST(y,m)  QUIT $ZDATEH(m=12+y*100+(m#12)+1*100+1,8)-1

<Nitpicking OFF>

By the way, if you are just interested, how many days a month in a given year has, there is a simple formula:

LastDay(y,m)  quit $s(m-2:m\8+m#2+30, 1:y#4=0+28)

In the above formula, it's OK to use short leap year calculation (y#4=0) for contemporary dates (date from 1901 until 2099) else case you have to stick to the long format: (y#4=0)-(y#100=0)+(y#400=0)

You could create an abstract class, which adds all the information you need to your class(es), see the example below.
Then include this class into the class(es) where you need this kind of information:

Class Your.Class Extends (%Persistent, DC.ClassInfo)  { ... }
Class Your.OtherClass Extends (%Persistent, DC.ClassInfo) { ... }

and use it as follows:

set allProps = ##class(Your.Class).PropertyInfo()
write allProps.%ToJSON() --> [prop1, prop2, ...]  // here you get the names of all properties
set oneProp = ##class(Your.Class).PropertyInfo("aPropertyName") --> {"Type":"%String"} // Info about one property

The same goes for the methods too.

Class definition for the (example) DC.ClassInfo class:

Class DC.ClassInfo [ Abstract ]
{
/// Return information about properties
/// 
/// 1) return list of all properties
///        obj.PropertyInfo() --> [Propertynam1, Propertyname2, Propertyname3, ...]
/// 
/// 2) return info for a specific property
///    obj.PropertyInfo(propertyname) --> {"Type":type, "MaxLen":nn, "Scale":n, ...}
///    
ClassMethod PropertyInfo(name = "") As %DynamicObject [ CodeMode = objectgenerator ]
{
    set prp=%compiledclass.Properties, all={}

    for i=1:1:prp.Count() {
        set p=prp.GetAt(i), inf={}
        // Add all the infos you need...
        set inf.Type=p.Type
        set x=p.Parameters.GetAt("MAXLEN") set:x]"" inf.MaxLen=x
        set x=p.Parameters.GetAt("SCALE") set:x]"" inf.Scale=x
        do all.%Set(p.Name,inf)
    }
    
    do %code.WriteLine($c(9)_"set prp="_all.%ToJSON())
    do %code.WriteLine($c(9)_"if name]"""" quit prp.%Get(name)")
    do %code.WriteLine($c(9)_"set itr=prp.%GetIterator(), names=[]")
    do %code.WriteLine($c(9)_"while itr.%GetNext(.k) { do names.%Push(k) }")
    do %code.WriteLine($c(9)_"quit names")
    quit $$$OK
}

/// Return information about properties
/// 
/// 1) return list of all properties
///        obj.PropertyInfo() --> [Propertynam1, Propertyname2, Propertyname3, ...]
/// 
/// 2) return info for a specific property
///    obj.PropertyInfo(propertyname) --> {"Type":type, "MaxLen":nn, "Scale":n, ...}
///    
ClassMethod MethodInfo(name = "") As %DynamicObject [ CodeMode = objectgenerator ]
{
    set mth=%compiledclass.Methods, all={}
    
    for i=1:1:mth.Count() {
        set m=mth.GetAt(i)
        // Add all the infos you need...            
        set inf={}, spc=m.FormalSpecParsed
        set inf.SqlProc=m.SqlProc            
        set inf.ClassMethod=m.ClassMethod
        set inf.ReturnType=m.ReturnType
        set inf.Args=[]
        for j=1:1:$ll(spc) {
            set arg={}, itm=$li(spc,j), arg.Name=$li(itm), arg.Type=$li(itm,2)
            set arg.ByRef=$li(itm,3)="&", arg.DefValue=$li(itm,4)
            do inf.Args.%Push(arg)
        }
        do all.%Set(m.Name,inf)
    }
    
    do %code.WriteLine($c(9)_"set mth="_all.%ToJSON())
    do %code.WriteLine($c(9)_"if name]"""" quit mth.%Get(name)")
    do %code.WriteLine($c(9)_"set itr=mth.%GetIterator(), names=[]")
    do %code.WriteLine($c(9)_"while itr.%GetNext(.k) { do names.%Push(k) }")
    do %code.WriteLine($c(9)_"quit names")
    quit $$$OK
}
}

The class should work, but it's only partially tested. Also, you may want to omit all the inherited properties and methods

set p=prp.GetAt(i)  continue:p.Origin '= %class.Name // skip inherited propps
set m=mth.GetAt(i) continue:m.Origin '= %class.Name // skip inherited methods

it's up to you. Also you can add other informations, depending on your needs, see the respective classes (%Dictionary.CompiledClass, %Dictionary.CompiledProperties and %Dictionary.CompiledMethods)

You are on a right way, but obviously you use indirection on local variables in block environment.

the wrong way:

Method testErr()
{
   set myvar(1)=123, ref=$na(myvar)
   set ref=$query(@ref)  -->  this gives you always: ref=""
}

The correct way

Method testOK() [ PublicList = myvar]
{
   new myvar
   set myvar(1)=123, ref=$name(myvar)
   set ref=$query(@ref) 
   write ref," ",@ref ---> myvar(1)," ",123
}

i.e. indirection needs variables with global scope

First, resetting a password means, the user gets a new password, in your use case, this is not an option.

Second, if you want (for whatever reason)  to validate the user in some stage of the application, then you must calculate the PBKDF2 from useres input (the password) and from (the stored) salt. The hash you get should be equal to the hash, storted in the database. PBKDF2 is a one way salted password hash.

By the way, you have to care about not to transfer the users (clear text) input to your computation over an unsecure way!

I think, the $zconvert() function will cover only the necessary entities. But you can use a simple method to convert characters to currently known(*) entities.

ClassMethod ToHTML(str)
{
   for i=$length(str):-1:1 set c=$ascii(str,i) set:$data(^entityChars(0,c),c) $extract(str,i)=c
   quit str
}

ClassMethod FromHTML(str)
{
   set i=0
   while $locate(str,"&[A-Za-z]+;",i,j,v) {
   set:$data(^entityChars(1,v),c) s=$length(v), $extract(str,j-s,j-1)=$c(c), j=j-s+1
   set i=j
   }
   quit str
}

I have a table (the ^entityChars() global) which contains more the 1400 entities. You can download the above class, together with the table from my FTP server (File: DC.Entity.xml):

Adr: ftp.kavay.at
Usr: dcmember
Psw: member-of-DC

A sample output:

USER>write ##class(DC.Entity).ToHTML("Flávio Lúcio Naves Júnior")
Fl&aacute;vio L&uacute;cio Naves J&uacute;nior
USER>write ##class(DC.Entity).FromHTML("Fl&aacute;vio L&uacute;cio Naves J&uacute;nior")
Flávio Lúcio Naves Júnior

(*) Currently known, because (1) I do not have all the currently known entities in my table and (2) with each new day, the W3C and the Unicode consortium can extend the current entity list.